基础系列(6)—— C#类和对象

一、类介绍

      类(class是C#类型中最基础的类型。类是一个数据结构,将状态(字段)和行为(方法和其他函数成员)组合在一个单元中。类提供了用于动态创建类实例的定义,也就是对象(object。类支持继承(inheritance多态(polymorphism,即派生类能够扩展和特殊化基类的机制。

     使用类声明可以创建新的类。类声明以一个声明头开始,其组成方式如下:先是指定类的特性和修饰符,后跟类的名字,基类(如果有的话)的名字,以及被该类实现的接口名。声明头后面就是类体了,它由一组包含在大括号({})中的成员声明组成。

下面是一个名为Point的简单类的声明:

public class Point
{
   public int x, y;
   public Point(int x, int y){
   this.x = x;
   this.y = y;
   }
}

     使用new运算符创建类的实例,它将为新实例分配内存,调用构造函数初始化实例,并且返回对该实例的引用。下面的语句创建两个Point对象,并且将那些对象的引用保存到两个变量中:

  Point p1 = new Point(0, 0);

  Point p2 = new Point(10, 20);

    当不再使用对象时,该对象所占的内存将被自动回收。在C#中,没有必要也不可能显式地释放对象。

二、类成员

    类的成员或者是静态成员(static member,或者是实例成员(instance member。静态成员属于类,实例成员属于对象(类的实例)。

    下表提供了类所能包含的各种成员的描述。

成    员

描    述

常数

与类关联的常量值

字段

类的变量

方法

能够被类执行的计算和行为

属性

使对象能够读取和写入类的命名属性

索引器

使对象能够用与数组相同的方式进行索引

事件

能够被类产生的通知

运算符

类支持的转换和表达式运算符

构造函数

初始化类的实例或者类本身

析构函数

在永久销毁类的实例之前执行的行为

类型

被类声明的嵌套类型

三、可访问性

    类的每个成员都有关联的可访问性,它控制能够访问该成员的程序文本区域。有5种可能的可访问性形式。表1.7概述了类的可访问性的意义。

可访问性

意    义

public

访问不受限制

protected

访问仅限于包含类或从包含类派生的类型

internal

访问仅限于当前程序集

protected internal

访问仅限于从包含类派生的当前程序集或类型

private

访问仅限于包含类

四、基类

    类的声明可能通过在类名后加上冒号和基类的名字来指定一个基类译注4。省略基类等同于直接从object类派生。在下面的示例中,Point3D的基类是Point,而Point的基类是object:

public class Point

{

   public int x, y;  

   public Point(int x, int y){

     this.x = x;

     this.y = y;

   }

}

public class Point3D: Point

{

  public int z; 

  public Point3D(int x, int y, int z): Point(x, y){

    this.z = z;

  }

}

     Point3D类继承了其基类的成员。继承意味着类将隐式地包含其基类的所有成员(除了基类的构造函数)。派生类能够在继承基类的基础上增加新的成员,但是它不能移除继承成员的定义。在前面的示例中,Point3D类从Point类中继承了x字段和y字段,并且每一个Point3D实例都包含三个字段x,y和z。

      从类类型到它的任何基类类型都存在隐式的转换。并且,类类型的变量能够引用该类的实例,或者任何派生类的实例。例如,对于前面给定的类声明,Point类型的变量能够引用Point实例或者Point3D实例:

Point a = new Point(10, 20);

Point b = new Point3D(10, 20, 30);

五、字段

     字段是与对象或类相关联的变量。当一个字段声明中含有static修饰符时,由该声明引入的字段为静态字段(static field。它只标识了一个存储位置。不管创建了多少个类实例,静态字段都只会有一个副本。

     当一个字段声明中不含有static修饰符时,由该声明引入的字段为实例字段(instance field。类的每个实例都包含了该类的所有实例字段的一个单独副本。

     在下面的示例中,Color类的每个实例都有r,g,b实例字段的不同副本,但是Black,White,Red,Green和Blue等静态字段只有一个副本:

public class Color
{

   public static readonly Color Black = new Color(0, 0, 0);

   public static readonly Color White = new Color(255, 255, 255);

   public static readonly Color Red = new Color(255, 0, 0);

   public static readonly Color Green = new Color(0, 255, 0);

   public static readonly Color Blue = new Color(0, 0, 255);

   private byte r, g, b;
public Color(byte r, byte g, byte b) { this.r = r; this.g = g; this.b = b; } }

    如前面的示例所示,通过readonly修饰符声明只读字段。给readonly字段的赋值只能作为声明的组成部分出现,或者在同一类中的实例构造函数或静态构造函数中出现。

六、方法

       方法(method是一种用于实现可以由对象或类执行的计算或操作的成员。静态方法(static method只能通过类来访问。实例方法(instance method则要通过类的实例访问。

       方法有一个参数(parameter列表(可能为空),表示传递给方法的值或者引用;方法还有返回类型(return type,用于指定由该方法计算和返回的值的类型。如果方法不返回一个值,则它的返回类型为void。

      在声明方法的类中,该方法的签名必须是惟一的。方法的签名由它的名称、参数的数目、每个参数的修饰符和类型组成。返回类型不是方法签名的组成部分。

(一)参数

    参数用于将值或者引用变量传递给方法。当方法被调用时,方法的参数译注5从指定的自变量(argument译注6得到它们实际的值。C#有4种参数:值参数、引用参数、输出参数和参数数组。

    值参数(value parameter用于输入参数的传递。值参数相当于一个局部变量,它的初始值是从为该参数所传递的自变量获得的。对值参数的修改不会影响所传递的自变量。

    引用参数(reference parameter用于输入和输出参数的传递。用于引用参数的自变量必须是一个变量,并且在方法执行期间,引用参数和作为自变量的变量所表示的是同一个存储位置。引用参数用ref修饰符声明。下面的示例展示了ref参数的使用:

using System;

class Test

{

   static void Swap(ref int x, ref int y) {

      int temp = x;

      x = y;

      y = temp;

   }

   static void Main() {

      int i = 1, j = 2;

      Swap(ref i, ref j);

      Console.WriteLine("{0} {1}", i, j);  //输出 "2 1"

   }

}

   输出参数(output parameter用于输出参数的传递。输出参数类似于引用参数,不同之处在于调用方提供的自变量初始值无关紧要。输出参数用out修饰符声明。下面的示例展示了out参数的使用:

using System;
class Test {
   static void Divide(int x, int y, out int result, out int remainder) {
result
= x / y; remainder = x % y; } static void Main() { int res, rem; Divide(10, 3, out res, out rem); Console.WriteLine("{0} {1}", res, rem); //输出 "3 1" } }

参数数组(parameter array允许将可变长度的自变量列表传递给方法。参数数组用params修饰符声明。只有方法的最后一个参数能够被声明为参数数组,而且它必须是一维数组类型。System.Console类的Write和WriteLine方法是参数数组应用的很好的例子。它们的声明形式如下:

  public class Console
   {
      public static void Write(string fmt, params object[] args) {...}
      public static void WriteLine(string fmt, params object[] args) {...}
      ...
   }

在方法中使用参数数组时,参数数组表现得就像常规的数组类型参数一样。然而,带数组参数的方法调用中,既可以传递参数数组类型的单个自变量,也可以传递参数数组的元素类型的若干自变量。对于后者的情形,数组实例将自动被创建,并且通过给定的自变量初始化。示例:

Console.WriteLine("x={0} y={1} z={2}", x, y, z);

等价于下面的语句:

object[] args = new object[3];

args[0] = x;

args[1] = y;

args[2] = z;

Console.WriteLine("x={0} y={1} z={2}", args);

(二)方法体和局部变量

  方法体指定方法调用时所要执行的语句。方法体能够声明特定于该方法调用的变量。这样的变量被称为局部变量(local variable。局部变量声明指定类型名、变量名,可能还有初始值。下面的示例声明了一个局部变量i,其初始值为0;另一个局部变量j没有初始值。

using System;
class Squares { static void Main() { int i = 0; int j; while(i < 10){ j = i * i; Console.WriteLine("{0} x {0} = {1}", i, j); i = i + 1; } } }

    C#要求局部变量在其值被获得之前明确赋值(definitely。例如,假设前面的变量i的声明没有包含初始值,那么,在接下来对i的使用将导致编译器报告错误,原因就是i在程序中没有明确赋值。

  方法能够使用return语句将控制返回给它的调用方。如果方法是void的,则return语句不能指定表达式;如果方法是非void的,则return语句必须包含表达式,用于计算返回值。

(三)静态方法和实例方法

      若一个方法声明中含有static修饰符,则称该方法为静态方法(static method。静态方法不对特定实例进行操作,只能访问静态成员。若一个方法声明中没有static修饰符,则称该方法为实例方法(instance method。实例方法对特定实例进行操作,既能够访问静态成员,也能够访问实例成员。在调用实例方法的实例上,可以用 this来访问该实例,而在静态方法中引用this是错误的。

下面的Entity类具有静态和实例两种成员:

class Entity
{
   static int nextSerialNo;
   int serialNo;
   public Entity() {
      serialNo = nextSerialNo++;
   }
 
   public int GetSerialNo() {
      return serialNo;
   }

  
   public static int GetNextSerialNo() {
     return nextSerialNo;
   }

   public static void SetNextSerialNo(int value) {
      nextSerialNo = value;
   }

}

     每一个Entity实例包含一个序列号(并且假定这里省略了一些其他信息)。Entity构造函数(类似于实例方法)用下一个有效的序列号初始化新的实例。因为构造函数是一个实例成员,所以,它既可以访问serialNo实例字段,也可以访问nextSerialNo静态字段。

GetNextSerialNo和SetNextSerialNo静态方法能够访问nextSerialNo静态字段,但是如果访问serialNo实例字段就会产生错误。

下面的示例展示了Entity类的使用:

using System;
class Test
{
   static void Main() {

       Entity.SetNextSerialNo(1000);

       Entity e1 = new Entity();

       Entity e2 = new Entity();

       Console.WriteLine(e1.GetSerialNo());              //输出 "1000"

       Console.WriteLine(e2.GetSerialNo());              //输出 "1001"

       Console.WriteLine(Entity.GetNextSerialNo());   //输出 "1002"

   }

}

注意,SetNextSerialNo和GetNextSerialNo静态方法通过类调用,而GetSerialNo实例成员则通过类的实例调用。

(四) 虚拟方法、重写方法和抽象方法

若一个实例方法的声明中含有virtual修饰符,则称该方法为虚拟方法(virtual method。若其中没有virtual修饰符,则称该方法为非虚拟方法(nonvirtual method

在一个虚拟方法调用中,该调用所涉及的实例的运行时类型(runtime type确定了要被调用的究竟是该方法的哪一个实现。在非虚拟方法调用中,实例的编译时类型(compile-time type)是决定性因素。

虚拟方法可以由派生类重写(override译注7实现。当一个实例方法声明中含有override修饰符时,该方法将重写所继承的相同签名的虚拟方法。虚拟方法声明用于引入新方法,而重写方法声明则用于使现有的继承虚拟方法专用化(通过提供该方法的新实现)。

抽象(abstract方法是没有实现的虚拟方法。抽象方法的声明是通过abstract修饰符实现的,并且只允许在抽象类中使用抽象方法声明。非抽象类的派生类需要重写抽象方法。

下面的示例声明了一个抽象类Expression,它表示一个表达式树的节点;它有三个派生类Constant,VariableReference,Operation,它们实现了常数、变量引用和算术运算的表达式树节点。

using System;

using System.Collections;

 

public abstract class Expression

{

   public abstract double Evaluate(Hashtable vars);

}

 

public class Constant: Expression

{

   double value;

   public Constant(double value) {

      this.value = value;

   }

   public override double Evaluate(Hashtable vars) {

      return value;

   }

}

 

public class VariableReference: Expression

{

   string name;

   public VariableReference(string name) {

      this.name = name;

   }

   public override double Evaluate(Hashtable vars) {

      object value = vars[name];

      if (value == null) {

          throw new Exception("Unknown variable: " + name);

      }

      return Convert.ToDouble(value);

   }

}

 

public class Operation: Expression

{

   Expression left;

   char op;

   Expression right;

   public Operation(Expression left, char op, Expression right) {

       this.left = left;

       this.op = op;

       this.right = right;

   }

    public override double Evaluate(Hashtable vars) {

      double x = left.Evaluate(vars);

      double y = right.Evaluate(vars);

      switch(op) {

          case '+' : return x + y;

          case '-' : return x - y;

          case '*' : return x * y;

          case '/' : return x / y;

      }

      throw new Exception("Unknown operator");

   }

}  

前面的4个类用于模型化算术表达式。例如,使用这些类的实例,表达式x+3能够被表示为如下的形式:

Expression e = new Operation(
   new VariableReference("x"),

   '+',

   new Constant(3));

Expression实例的Evaluate方法将被调用,以计算表达式的值,从而产生一个double值。该方法取得一个包含变量名(输入的键)和值(输入的值)的Hashtable作为其自变量。Evaluate方法是虚拟的抽象方法,意味着派生类必须重写它并提供实际的实现。

Evaluate方法的Constant的实现只是返回保存的常数。VariableReference的实现在Hashtable中查找变量名,并且返回相应的值。Operation的实现则首先计算左操作数和右操作数的值(通过递归调用Evaluate方法),然后执行给定的算术运算。

下面的程序使用Expression类,对于不同的xy的值,计算表达式x*(y+2)。

using System;

using System.Collections;

class Test

{

   static void Main() {

      Expression e = new Operation(

         new VariableReference("x"),

         '*',

         new Operation(

            new VariableReference("y"),

            '+',

            new Constant(2)

         )

);

Hashtable vars = new Hashtable();

      Vars["x"] = 3;

      Vars["y"] = 5;

      Console.WriteLine(e.Evaluate(vars));   //输出 "21"

      Vars["x"] = 1.5;

      Vars["y"] = 9;

      Console.WriteLine(e.Evaluate(vars));   //输出 "16.5"

   }

}

(五)方法重载

方法重载(Method overloading允许在同一个类中采用同一个名称声明多个方法,条件是它们的签名是惟一的。当编译一个重载方法的调用时,编译器采用重载决策(overload resolution确定应调用的方法。重载决策找到最佳匹配自变量的方法,或者在没有找到最佳匹配的方法时报告错误信息。下面的示例展示了重载决策工作机制。在Main方法中每一个调用的注释说明了实际被调用的方法。

class Test

{

   static void F() {

      Console.WriteLine("F()");

   }

   static void F(object x) {

      Console.WriteLine("F(object)");

   }

   static void F(int x) {

      Console.WriteLine("F(int)");

   } 

   static void F(double x) {

      Console.WriteLine("F(double)");

   }  

   static void F(double x, dpuble y) {

      Console.WriteLine("F(double, double)");

   }  

   static void Main(){

      F();                  //调用F()

      F(1);                 //调用F(int)

      F(1.0);               //调用F(double)

      F("abc");            //调用F(object)

      F((double)1);        //调用F(double)

      F((object)1);        //调用F(object)

      F(1, 1);               //调用F(double, double)

   }

}

如上例所示,总是通过自变量到参数类型的显式的类型转换,来选择特定方法。

(六) 其他函数成员

类的函数成员(function member是包含可执行语句的成员。前面部分所描述的方法是主要的函数成员。这一节讨论其他几种C#支持的函数成员:构造函数、属性、索引器、事件、运算符、析构函数。

public class List

{

   const int defaultCapacity = 4;

常数

   object[] items;

   int count;

字段

   public List(): this(defaultCapacity) {}

   public List(int capacity) {

      items = new object[capacity];

   }

构造函数

   public int Count {

      get { return count; }

   }

   public string Capacity {

      get {

         return items.Length;

      }

 

      set {

 

         if (value < count) value = count;

         if (value != items.Length) {

            object[] newItems = new object[value];

            Array.Copy(items, 0, newItems, 0, count);

            items = newItems;

         }

      }

   }

属性

 

   public object this[int index] {

      get {

          return items[index];

      }

      set {

          items[index] = value;

          OnListChange();

      }

   }

索引器

   public void Add(object item) {

      if (count == Capacity) Capacity = count * 2;

      items[count] = item;

      count++;

      OnChanged();

   }

   protected virtual void OnChanged() {

      if (Changed != null) Changed(this, EventArgs.Empty);

   }

   public override bool Equals(object other) {

return Equals (this,other as List );

   }

static bool Equals ( List a,List b) {

      if (a == null) return b == null;

      if (b == null || a.count != b.count) return false;

      for (int i = 0; i < a.count; i++) {

         if (!object.Equals(a.item[i], b.item[i])) {

            return false;

         }

      }

   }

方法

   public event EventHandler Changed;

事件

   public static bool operator ==(List a, List b) {

      return Equals(a, b);

   }

   public static bool operator !=(List a, List b) {

      return !Equals(a, b);

   }

运算符

}

(七)  构造函数

 

C#既支持实例构造函数,也支持静态构造函数。实例构造函数(instance constructor是实现初始化类实例所需操作的成员。静态构造函数(static constructor是一种在类首次加载时用于实现初始化类本身所需操作的成员。

 

构造函数的声明如同方法一样,不过,它没有返回类型,它的名字与包含它的类名一样。若构造函数的声明中包含static修饰符,则它声明了一个静态构造函数,否则声明实例构造函数。

 

实例构造函数能够被重载。例如,List声明了两个实例构造函数,一个不带参数,一个带有一个int参数。使用new运算符可以调用实例参数。下面的语句使用各个List类的构造函数创建了两个List实例。

 

List list1 = new List();

List list2 = new List(10);

 

实例构造函数不同于其他方法,它是不能被继承的。并且,一个类除了自己声明的实例构造函数外,不可能有其他的实例构造函数。如果一个类没有声明任何实例构造函数,则会自动地为它提供一个默认的空的实例构造函数。

 

八 、方法的参数类型

(一) 两类参数的定义:

实参:具有实际意义的参数,一般在调用方法时,在方法的括号里面传递的参数为实参

形参:在方法声明时,方法名后面括号里面的参数为形参,一般(值传递)形参是接收实参传过来的值

(二) out参数:

1、定义:

       在一个方法需要返回多个参数的时候,一般在参数列表里面声明out类型的参数,以便输出多个返回值。其中,out参数只负责把结果输出,不负责输入参数。一般out声明的变量需要在方法体内部赋值

2、例子:

class Program
    {
        static void Main(string[] args)
        {
            int[] ShuZi = { 1,2,3,4,5,6};
            int max1;
            int min1;
            int sum1;
            JiSuan(ShuZi,out max1,out min1,out sum1);
            Console.WriteLine("数组的最大值为:{0},数组的最小值为:{1},数组的和为:{2}",max1,min1,sum1);
            Console.ReadKey();
        }
        public static void JiSuan(int []number,out int max,out int  min,out int sum)
        {
            max = number[0];
            min = number[0];
            sum = 0;
            for (int i = 0; i < 6; i++)
            {
                sum += number[i];
                if (number[i] > max)
                {
                    max = number[i];
                }
                else
                {
                min=number[i];
                }
            }
        }
    }

输出结果为:最大值:6,最小值:1,和为21 

3 注意

   out指定的参数必须在函数定义的时候就赋初值。否则则出现错误。对比ref指定的参数则可以不在函数内部进行赋初值,在函数调用时候再赋初值也可以。

(二)ref参数

1、定义:

ref 关键字使参数按引用传递。其效果是,当控制权传递回调用方法时,在方法中对参数所做的任何更改都将反映在该变量中。若要使用 ref 参数,则方法定义和调用方法都必须显式使用 ref 关键字。也即是说,在方法中对参数的设置和改变将会直接影响函数调用之处(代码①及②)。无论是函数的定义还是调用时均不可忽略关键字ref.可以对比代码:
代码①:
复制代码
 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Program pg = new Program();
 6             int x = 10;
 7             int y = 20;
 8             pg.GetValue(ref x, ref  y);
 9             Console.WriteLine("x={0},y={1}", x, y);
10 
11             Console.ReadLine();
12           
13         }
14 
15         public void GetValue(ref int x, ref int y)
16         {
17             x = 521;
18             y = 520;
19         }
20     }
复制代码
运行结果为

 

代码②:

复制代码
 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Program pg = new Program();
 6             int x = 10;
 7             int y = 20;
 8             pg.GetValue(ref x, ref  y);
 9             Console.WriteLine("x={0},y={1}", x, y);
10 
11             Console.ReadLine();
12           
13         }
14 
15         public void GetValue(ref int x, ref int y)
16         {
17             x = 1000;
18             y = 1;
19         }
20     }
复制代码

由代码① 和②的运行结果可以看出,在方法中对参数所做的任何更改都将反映在该变量中,而在main函数中对参数的赋值却没有起到作用,这是不是说明不需要进行初始化呢?来看第二点

2 注意
ref定义的参数在使用前必须初始化,无论在函数定义时候参数有没有赋予初值。这条正好区分out指定的参数可以不在调用函数的时候进行初始化。
来看代码③ 以及其运行结果:
复制代码
 1  class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Program pg = new Program();
 6             int x;
 7             int y;        //此处x,y没有进行初始化,则编译不通过。
 8             pg.GetValue(ref x, ref  y);
 9             Console.WriteLine("x={0},y={1}", x, y);
10 
11             Console.ReadLine();
12           
13         }
14 
15         public void GetValue(ref int x, ref int y)
16         {
17             x = 1000;
18             y = 1;
19         }    
20     }
复制代码

出现的错误为:使用了未赋值的局部变量“x”,“y”。故可以说明ref指定的参数无论在函数定义的时候有没有赋予初值,在使用的时候必须初始化。

(四)总结

ref和out使用时的区别是:
①:ref指定的参数在函数调用时候必须初始化,不能为空的引用。而out指定的参数在函数调用时候可以不初始化;
②:out指定的参数在进入函数时会清空自己,必须在函数内部赋初值。而ref指定的参数不需要。

参考资料:《c#图解教程》

posted @ 2018-03-26 23:24  StrugglingDave  阅读(1805)  评论(0编辑  收藏  举报