c#----类和对象

1.1 类和对象

类(class)是最基础的c#类型。类是一个数据结构,将状态(字段)和操作(方法和其它函数成员)组合在一个单元中。类为动态创建的类实例(instance)提供了定义,实例也称对象(object).类支持继承(inheritance)和多态性(polymorphism),这是派生类(derived class)可用来扩展和专用化基类(base class)的机制。

使用类声明可以创建新的类。类声明以一个声明头开始,其组成方式如下:先指定类的属性和修饰符,然后是类的名称,接着是基类以及该类实现的接口。声明头后面跟着类体,它由一组位于一对大括号之间的成员声明组成。

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

public class Student
{
    public int id,age;
    
    public Point(int id,int age)
    {
        this.id = id;
        this.age = age;
    }
}

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

Student s1= new Student (1,10);

Student s2 = new Student (2,12);

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

1.1.1 成员

class 的成员要么是静态成员,要么是实例成员。 静态成员属于类,而实例成员则属于对象(类实例)。

以下列表概述了类可以包含的成员类型。

  • 常量:与类相关联的常量值
  • 字段:与类关联的变量
  • 方法:类可执行的操作
  • 属性:与读取和写入类的已命名属性相关联的操作
  • 索引器:与将类实例编入索引(像处理数组一样)相关联的操作
  • 事件:类可以生成的通知
  • 运算符:类支持的转换和表达式运算符
  • 构造函数:初始化类实例或类本身所需的操作
  • 终结器:永久放弃类的实例之前完成的操作
  • 类型:类声明的嵌套类型

1.1.2 可访问性 

每个类成员都有关联的可访问性,用于控制能够访问成员的程序文本区域。 可访问性有六种可能的形式。 以下内容对访问修饰符进行了汇总。

  • public:访问不受限制。
  • private:访问仅限于此类。
  • protected:访问仅限于此类或派生自此类的类。
  • internal:仅可访问当前程序集(.exe 或 .dll)。
  • protected internal:仅可访问此类、从此类中派生的类,或者同一程序集中的类。
  • private protected:仅可访问此类或同一程序集中从此类中派生的类。

1.1.3 基类

类声明可通过在类名称后面加上一个冒号和基类的名称来指定一个基类。省略基类的指定等同于从类型 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 类从 Point 类继承了 x 字段和 y 字段,每个 Point3D 实例都包含三个字段 x、y 和 z。

从某个类类型到它的任何基类类型存在隐式的转换。因此,类类型的变量可以引用该类的实例或任何派生类的实例。例如,对于前面给定的类声明,Point 类型的变量既可以引用 Point 也可以引用 Point3D:

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

1.1.4 字段

字段 是与类或类实例相关联的变量。

使用静态修饰符声明的字段定义的是静态字段(static field)。 静态字段只指明一个存储位置。 无论创建多少个类实例,永远只有一个静态字段副本。

不使用静态修饰符声明的字段定义的是实例字段(instance field)。 每个类实例均包含相应类的所有实例字段的单独副本。

在以下示例中,每个 Color 类实例均包含 RG 和 B 实例字段的单独副本,但只包含 BlackWhiteRedGreen 和 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);
    
    public byte R;
    public byte G;
    public byte B;

    public Color(byte r, byte g, byte b)
    {
        R = r;
        G = g;
        B = b;
    }
}

 如上面的示例所示,可以使用 readonly 修饰符声明 只读字段 (read-only field)。 只能在字段声明期间或在同一个类的构造函数中向只读字段赋值。

1.1.5 方法

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

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

方法的签名 (signature) 在声明该方法的类中必须唯一。方法的签名由方法的名称、类型参数数量及其参数的数目、修饰符和类型组成。方法的签名不包含返回类型。

当方法主体是单个表达式时,可使用紧凑表达式格式定义方法,如下例中所示:

public override string ToString() => "This is an object";

1.1.5.1 参数

参数用于向方法传递值或变量引用。方法的参数从方法被调用时指定的实参 (argument) 获取它们的实际值。有四种类型的参数:值参数、引用参数、输出参数和参数数组。

值参数 (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);           // Outputs "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);   // Outputs "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);

1.1.5.2 方法体和局部变量 

方法主体指定了在调用方法时执行的语句。

方法主体可以声明特定于方法调用的变量。 此类变量称为 局部变量。 局部变量声明指定了类型名称、变量名称以及可能的初始值。 下面的示例声明了初始值为零的局部变量 i 和无初始值的局部变量 j

class Squares
{
    public static void WriteSquares()
    {
        int i = 0;
        int j;
        while (i < 10)
        {
            j = i * i;
            Console.WriteLine($"{i} x {i} = {j}");
            i = i + 1;
        }
    }
}

C# 要求必须先 明确赋值 局部变量,然后才能获取其值。 例如,如果上述 i 的声明未包含初始值,那么编译器会在后续使用 i 时报告错误,因为在后续使用时 i 不会在程序中得到明确赋值。

方法可以使用 return 语句将控制权返回给调用方。 在返回 void 的方法中,return 语句无法指定表达式。 在不返回 void 的方法中,return 语句必须包括用于计算返回值的表达式。

 1.1.5.3 静态方法和实例方法

使用 static 修饰符声明的方法是静态方法。 静态方法不对特定的实例起作用,只能直接访问静态成员。

未使用 static 修饰符声明的方法是实例方法。 实例方法对特定的实例起作用,并能够访问静态和实例成员。 其中调用实例方法的实例可以作为 this 显式访问。 在静态方法中引用 this 会生成错误。

以下 Entity 类包含静态和实例成员。

class Entity
{
    static int s_nextSerialNo;
    int _serialNo;
    
    public Entity()
    {
        _serialNo = s_nextSerialNo++;
    }
    
    public int GetSerialNo()
    {
        return _serialNo;
    }
    
    public static int GetNextSerialNo()
    {
        return s_nextSerialNo;
    }
    
    public static void SetNextSerialNo(int value)
    {
        s_nextSerialNo = value;
    }
}

每个 Entity 实例均有一个序列号(很可能包含此处未显示的其他一些信息)。 Entity 构造函数(类似于实例方法)将新实例初始化为包含下一个可用的序列号。 由于构造函数是实例成员,因此可以访问 _serialNo 实例字段和 s_nextSerialNo 静态字段。

GetNextSerialNo 和 SetNextSerialNo 静态方法可以访问 s_nextSerialNo 静态字段,但如果直接访问 _serialNo 实例字段,则会生成错误。

下例显示了 Entity 类的用法。

Entity.SetNextSerialNo(1000);
Entity e1 = new Entity();
Entity e2 = new Entity();
Console.WriteLine(e1.GetSerialNo());          // Outputs "1000"
Console.WriteLine(e2.GetSerialNo());          // Outputs "1001"
Console.WriteLine(Entity.GetNextSerialNo());  // Outputs "1002"

 SetNextSerialNo 和 GetNextSerialNo 静态方法在类中进行调用,而 GetSerialNo 实例方法则是在类实例中进行调用。

 1.1.5.4 虚方法、重写方法和抽象方法

可使用虚方法、重写方法和抽象方法来定义类类型层次结构的行为。 由于类可从基类派生,因此这些派生类可能需要修改在基类中实现的行为。 虚方法是在基类中声明和实现的方法,其中任何派生类都可提供更具体的实现。 重写方法是在派生类中实现的方法,可修改基类实现的行为。 抽象方法是在基类中声明的方法,必须在所有派生类中重写。 事实上,抽象方法不在基类中定义实现。

对实例方法的方法调用可解析为基类或派生类实现。 变量的类型确定了其编译时类型。 编译时类型是编译器用于确定其成员的类型。 但是,可将变量分配给从其编译时类型派生的任何类型的实例。 运行时类型是变量所引用的实际实例的类型。

调用虚方法时,为其调用方法的实例的 运行时类型 决定了要调用的实际方法实现代码。 调用非虚方法时,实例的 编译时类型 是决定性因素。

可以在派生类中 重写 虚方法。 如果实例方法声明中有 override 修饰符,那么实例方法可以重写签名相同的继承虚方法。 虚方法声明引入了新方法。 重写方法声明通过提供现有继承的虚方法的新实现,专门针对该方法。

抽象方法 是没有实现代码的虚方法。 抽象方法使用 abstract 修饰符进行声明,仅可在抽象类中使用。 必须在所有非抽象派生类中重写抽象方法。

1.1.5.5 方法重载

借助方法 重载,同一类中可以有多个同名的方法,只要这些方法具有唯一签名即可。 编译如何调用重载的方法时,编译器使用 重载决策 来确定要调用的特定方法。 重载决策会查找与自变量匹配度最高的一种方法。 如果找不到任何最佳匹配项,则会报告错误。 下面的示例展示了重载决策的实际工作方式。 UsageExample 方法中每个调用的注释指明了调用的方法。

class OverloadingExample
{
    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<T>(T x) => Console.WriteLine("F<T>(T)");            
    static void F(double x, double y) => Console.WriteLine("F(double, double)");
    
    public static void UsageExample()
    {
        F();            // Invokes F()
        F(1);           // Invokes F(int)
        F(1.0);         // Invokes F(double)
        F("abc");       // Invokes F<string>(string)
        F((double)1);   // Invokes F(double)
        F((object)1);   // Invokes F(object)
        F<int>(1);      // Invokes F<int>(int)
        F(1, 1);        // Invokes F(double, double)
    }
}

如示例所示,可将自变量显式转换成确切的参数类型和类型自变量,随时选择特定的方法。

1.1.6 其它函数成员

包含可执行代码的成员统称为类的 函数成员。 上一部分介绍了作为主要函数成员类型的方法。 此部分将介绍 C# 支持的其他类型函数成员:构造函数、属性、索引器、事件、运算符和终结器。

下面的示例展示了 MyList<T> 泛型类,用于实现对象的可扩充列表。 此类包含最常见类型函数成员的多个示例。

public class List
{

    const int defaultCapacity = 4;

常量

    object[] items;
    int count;

字段

    public List(): List(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.items[i], b.items[i])) {
              return false;
           }
       }
      return true;
    }

方法

    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# 支持实例和静态构造函数。 实例构造函数 是实现初始化类实例所需执行的操作的成员。 静态构造函数是实现在首次加载类时初始化类本身所需执行的操作的成员。

构造函数的声明方式与方法一样,都没有返回类型,且与所含类同名。 如果构造函数声明包含 static 修饰符,则声明的是静态构造函数。 否则,声明的是实例构造函数。

实例构造函数可重载并且可具有可选参数。 例如,MyList<T> 类声明一个具有单个可选 int 参数的实例构造函数。 实例构造函数使用 new 运算符进行调用。 下面的语句使用包含和不包含可选自变量的 MyList 类构造函数来分配两个 MyList<string> 实例。

MyList<string> list1 = new MyList<string>();
MyList<string> list2 = new MyList<string>(10);

与其他成员不同,实例构造函数不会被继承。 类中只能包含实际上已在该类中声明的实例构造函数。 如果没有为类提供实例构造函数,则会自动提供不含参数的空实例构造函数。

1.1.6.2 属性

属性 (propery) 是字段的自然扩展。属性和字段都是命名的成员,都具有相关的类型,且用于访问字段和属性的语法也相同。然而,与字段不同,属性不表示存储位置。相反,属性有访问器 (accessor),这些访问器指定在它们的值被读取或写入时需执行的语句。

属性的声明与字段类似,不同的是属性声明以位于定界符 { 和 } 之间的一个 get 访问器和/或一个 set 访问器结束,而不是以分号结束。同时具有 get 访问器和 set 访问器的属性是读写属性 (read-write property),只有 get 访问器的属性是只读属性 (read-only property),只有 set 访问器的属性是只写属性 (write-only property)

get 访问器相当于一个具有属性类型返回值的无参数方法。除了作为赋值的目标,当在表达式中引用属性时,将调用该属性的 get 访问器以计算该属性的值。

set 访问器相当于具有一个名为 value 的参数并且没有返回类型的方法。当某个属性作为赋值的目标被引用,或者作为 ++ 或 -- 的操作数被引用时,将调用 set 访问器,并传入提供新值的实参。

List 类声明了两个属性 Count 和 Capacity,它们分别是只读属性和读写属性。下面是这些属性的使用示例。

List names = new List();
names.Capacity = 100;        // Invokes set accessor
int i = names.Count;             // Invokes get accessor
int j = names.Capacity;          // Invokes get accessor

与字段和方法相似,C# 同时支持实例属性和静态属性。静态属性使用 static 修饰符声明,而实例属性的声明不带该修饰符。

属性的访问器可以是虚的。当属性声明包括 virtual、abstract 或 override 修饰符时,修饰符应用于该属性的访问器。

1.1.6.3 索引器

借助 索引器 成员,可以将对象编入索引(像处理数组一样)。 索引器的声明方式与属性类似,不同之处在于,索引器成员名称格式为 this 后跟在分隔符 [ 和 ] 内写入的参数列表。 这些参数在索引器的访问器中可用。 类似于属性,索引器分为读写、只读和只写索引器,且索引器的访问器可以是虚的。

MyList<T> 类声明一个需要使用 int 参数的读写索引器。 借助索引器,可以使用 int 值将 MyList<T> 实例编入索引。 例如:

MyList<string> names = new MyList<string>();
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
for (int i = 0; i < names.Count; i++)
{
    string s = names[i];
    names[i] = s.ToUpper();
}

1.1.6.4 事件

借助 事件 成员,类或对象可以提供通知。 事件的声明方式与字段类似,区别是事件声明包括 event 关键字,且类型必须是委托类型。

在声明事件成员的类中,事件的行为与委托类型的字段完全相同(前提是事件不是抽象的,且不声明访问器)。 字段存储对委托的引用,委托表示已添加到事件的事件处理程序。 如果没有任何事件处理程序,则字段为 null

MyList<T> 类声明一个 Changed 事件成员,指明已向列表添加了新项。 Changed 事件由 OnChanged 虚方法引发,此方法会先检查事件是否是 null(即不含任何处理程序)。 引发事件的概念恰恰等同于调用由事件表示的委托。 不存在用于引发事件的特殊语言构造。

客户端通过 事件处理程序 响应事件。 使用 += 和 -= 运算符分别可以附加和删除事件处理程序。 下面的示例展示了如何向 MyList<string> 的 Changed 事件附加事件处理程序。

class EventExample
{
    static int s_changeCount;
    
    static void ListChanged(object sender, EventArgs e)
    {
        s_changeCount++;
    }
    
    public static void Usage()
    {
        var names = new MyList<string>();
        names.Changed += new EventHandler(ListChanged);
        names.Add("Liz");
        names.Add("Martha");
        names.Add("Beth");
        Console.WriteLine(s_changeCount); // "3"
    }
}

 1.1.6.5 运算符

运算符 是定义向类实例应用特定表达式运算符的含义的成员。 可以定义三种类型的运算符:一元运算符、二元运算符和转换运算符。 所有运算符都必须声明为 public 和 static

MyList<T> 类会声明两个运算符:operator == 和 operator !=。 对于向 MyList 实例应用这些运算符的表达式来说,这些重写的运算符向它们赋予了新的含义。 具体而言,这些运算符定义的是两个 MyList<T> 实例的相等性(使用其 Equals 方法比较所包含的每个对象)。 下面的示例展示了如何使用 == 运算符比较两个 MyList<int> 实例。

MyList<int> a = new MyList<int>();
a.Add(1);
a.Add(2);
MyList<int> b = new MyList<int>();
b.Add(1);
b.Add(2);
Console.WriteLine(a == b);  // Outputs "True"
b.Add(3);
Console.WriteLine(a == b);  // Outputs "False"

第一个 Console.WriteLine 输出 True,因为两个列表包含的对象不仅数量相同,而且值和顺序也相同。 如果 MyList<T> 未定义 operator ==,那么第一个 Console.WriteLine 会输出 False,因为 a 和 b 引用不同的 MyList<int> 实例。

1.1.6.6 终结器

终结器 是实现销毁类实例所需的操作的成员。 通常,需要使用终结器来释放非托管资源。 终结器既不能包含参数和可访问性修饰符,也不能进行显式调用。 实例的终结器在垃圾回收期间自动调用。

垃圾回收器在决定何时收集对象和运行终结器时有很大自由度。 具体而言,终结器的调用时间具有不确定性,可以在任意线程上执行终结器。 因为这样或那样的原因,只有在没有其他可行的解决方案时,类才能实现终结器。

处理对象析构的更好方法是使用 using 语句。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值