导航

Learning Hard C# 学习笔记: 4.C#中的类

Posted on 2023-10-05 11:24  ErgoCogito  阅读(28)  评论(0编辑  收藏  举报

类是面向对象语言都有的一种数据类型, 它的存在在于将现实中的概念抽象概括为代码中的数据类型.

4.1 什么是类?

以人类这个概念为例, 人类就可以作为一个, 人类是一个种群, 这个种群中包包含许多个体, 这些个体可以当作一个对象. 比如说小明就是人类中的一个个体, 他是人类这个概念具体化之后推导而来的, 这个具体化的过程在代码中叫做实例化,类实例化以后可以得到一个对象.
但同时人类有一些特性是每个个体共同持有的, 比如性别, 年龄, 姓名, 行为(吃饭, 工作, 思考和睡觉等)

4.2 C#如何定义一个类?

使用class关键字按照如下步骤操作即可:

class Human
{
    //类成员定义
}

上面代码定义了一个Human类. 定义之后可以在本项目或者其他项目通过类的实例化的方式来访问该类, 但并不是所有的类都能被这样访问.

默认情况下, 如果class关键字前没有添加访问修饰符, 则类的访问修饰符为internal, 表示仅在当前项目内可以被访问. 添加也行, 但没必要.

除了internal之外, 还有其他访问修饰符可以添加到类前面, 如下表.

修饰符 访问权限
无或internal 只能在同一程序集中访问类
public 同一程序集或引用该程序集的其他程序集都可以访问该类
abstract或internal abstract 只能在同一程序集中访问类,该类不能被实例化只能被继承
public abstract 同一程序集或引用该程序集的其他程序集都可以访问类,不能被实例化,只能被继承
sealed 或internal sealed 只能在同一程序集中访问类,该类不能被继承,只能被实例化
public sealed 同一程序集或引用该程序集的其他程序集都可以访问类,不能被继承,只能被实例化

4.3 类的成员

类的成员包括字段,属性,方法和构造函数等,这些与类一样有自己的访问权限。可以为它们指定访问修饰符,具体如下表,也可以使用static关键字将其声明为类的静态成员。静态成员属于类级别的概念,不属于类的实例(对象)。

修饰符 访问权限
public 同一程序集或引用该程序集的其他程序集都可以访问
private 只有同一个类中可以访问
protected 只有同一个类或派生类中可以访问
internal 只有同一程序集中可以访问
protected internal 在同一个程序集、该类和派生类中可以访问

4.3.1 字段

字段的定义由三部分组成——访问修饰符,字段的类型和字段名称,以下是定义方法。

public class Person
{
    // 姓名,类型为字符串类型
    private string name;
    // 年龄,类型为int类型
    public int age;
    // 性别,类型为bool类型
    protected bool sex;
}

可以使用关键字readonly 或const 来定义字段。若使用readonly 修饰,表示该字段是只读的,若使用const 修饰,则表示该字段是不变的。

public class Person
{
    private readonly string name;

    public const int age = 18;
    protected bool sex; // 包含字段
}

以上代码中,若使用readonly 修饰字段,则不需要在定义时初始化,而是可以在构造函数中再完成初始化;但若使用const 修饰字段,如果没有在定义字段时初始化,就会产生编译错误,错误信息为:"常量字段要求提供一个值" 。

也可以使用static 关键字来声明静态字段。静态字段与实例字段的区别在于——静态字段必须通过类来访问,而实例字段则需要通过类的对象实例来进行访问。下面是静态字段的定义方法。

public class Person
{
    // 静态字段定义,多了关键字static
    public static string name;

    // 实例成员定义
    public int age;
}

以上代码中定义的静态字段name 只能通过类名,即Person.name 的方式来访问;而对于实例字段age ,则不能这样访问,应通过类的实例对象,即new Person().age 的方式来访问(new Person() 表示实例化一个Person 对象)。

4.3.2 属性

属性是对字段的扩展。根据面向对象语言的封装思想(更多内容见第5章),字段最好设为private ,因为这样可以防止客户端直接对字段进行篡改,从而保证了内部成员的完整性。为了访问类中的私有字段,C#提供了属性这种机制,用来对字段进行灵活的控制和访问。

下面代码演示了属性的定义:

public class Person
{
    // 私有字段定义
    private string name;

    // 公有属性定义
    public string Name
    {
        // get访问器
        get
        {
            return name;
        }
        // set访问器
        set
        {
            // value是隐式参数
            name = value;
        }
    }
}

属性定义主要由get 访问器和set 访问器组成,get 访问器负责对字段值进行读取,set 访问器负责为字段进行赋值(它通过value 隐式参数来表示用户传入的值)。这里,get 访问器和set 访问器可以理解为两个方法,一个是用来返回字段,一个用来把用户传入的值赋给字段。

上面代码中, 我们定义的是一个可读写属性,因为它同时包含get 访问器和set 访问器。当属性仅包含get 访问器,或set 访问器为private 级别时,这样的属性被称为只读属性;当属性仅包含set 访问器,或get 访问器为private 级别时,这样的属性被称为只写属性。只读属性和只写属性的定义如下所示:

public class Person
{
    // 私有字段
    private string name;
    private int age;
    private bool sex;

    // 只读属性定义
    public string Name // 属性类型必须与访问的字段类型一样
    {
        get
        {
            return name;
        }
        // 私有set访问器
        private set
        {
            name = value;
        }
    }

    // 只读属性定义
    // 不包含set访问器
    public int Age
    {
        // get访问器
        get
        {
            return age;
        }
    }

    // 只写属性
    public bool Sex
    {
        // 私有get访问器
        private get
        {
            return sex;
        }
        set
        {
            sex = value;
        }
    }
}

如果不想使用Get和Set访问器,可以这样做:

public class Person
{
    // 私有字段定义
    private string name;

    // 返回字段
    public string getname()
    {
        return name;
    }

    // 为字段赋值
    public void setname(string value)
    {
        name = value;
    }
}

为了使字段的访问过程更加简单, 所以才会有属性的概念

属性除了能直接访问私有字段外,还可以根据需要加入更多的逻辑控制代码。例如,人的年龄一般在0~120岁之间,当代码试图将这个范围之外的数值赋给年龄字段时,则可在属性中添加抛出异常的代码来对错误进行处理。

public class Person
{
    private int age;

    public int Age
    {
        // get访问器
        get
        {
            return age;
        }
        set
        {
            // 在set访问器中添加更多的逻辑代码
            if(value<0 || value>120)
            {
                //抛出异常
                throw (new ArgumentOutOfRangeException("AgeIntPropery",value,"年龄必须在0-120之间"));
            }
            age=value;
        }
    }
}

和静态字段类似,属性也可通过static 关键字声明为静态属性。此时的静态属性属于类级别,不能通过类的实例进行访问,也不能在静态属性中使用非静态的字段。下面的代码演示了静态属性的定义。

public class Person
{
      private static string name;

    // 静态属性
    public static string Name
    {
        // get访问器
        get
        {
            return name;
        }
        // set访问器
        set
        {
            name = value;
        }
    }
}

4.3.3 方法

方法由方法签名和一系列语句的代码块组成. 其中, 方法签名包括方法的访问级别(例如public 或private )、可修饰符(例如abstract 关键字)、方法名称和参数。

Main 方法是每个C#应用程序的入口点,在启动应用程序时,Main 由公共语言运行时(CLR)负责调用。下面的代码演示了方法的定义:

public class Person
{
    // 类中定义了一个没有返回值的打印方法
    // name是用户传入的参数
    public void Print(string name)
    {
        // 输出传入的值
        Console.WriteLine("输入的值为:" + name);
    }
}

以上代码中,我们定义了一个用于打印的方法,此方法由以下几部分组成。

  • 方法的访问级别,代码中为public 。

  • 返回类型为void 。如果一个方法需要返回字符串类型,则返回类型为string ,且还需添加return 语句。

  • 方法名称,代码中为Print 。方法名称的命名要尽量有意义,因为这样可以让其他开发人员一看到方法名称就大致明白方法的用途。

  • 方法的参数。代码中方法的参数只有一个,即类型为string 的name 。

  • 一系列语句的代码块。上面代码中,代码块只有一句输出语句。

如何在其他地方调用定义好的方法。我们可以通过使用方法名称,并传入方法的参数个数和对应的类型来对方法进行调用,下面的代码演示了在Main中调用Print方法的过程。

class Program
{
    // 在Main方法中调用Person类中的Print方法
    static void Main(string[] args)
    {
        // 初始化类的实例
        Person p = new Person();

        // 通过使用方法名称,并传入对应的参数个数和类型来对方法进行调用
        p.Print("张三");
    }
}

上面代码中,由于定义的是实例方法,你需要在Main 调用Print 方法之前,首先实例化一个Person 对象,然后再通过该实例对方法进行调用。

静态字段和静态属性一样,方法也可以被static 关键字声明为静态方法。此时的静态方法属于类级别,不能通过类的实例进行访问。下面的代码演示了静态方法的定义和调用。

class Program
{
    // 静态方法直接通过类进行调用
    static void Main(string[] args)
    {
        // Print被static关键字定义为静态方法,需通过Person类直接进行调用
        Person.Print("张三");
    }
}

public class Person
{
    // 类中静态方法的定义,与实例方法不同的是多了static关键字
    public static void Print(string name)
    {
        // 输出传入的值
        Console.WriteLine("输入的值为:" + name);
    }
}

同样,C#也支持方法重载。方法重载指的是在类中可以定义多个名称相同但方法签名不同的方法。这里,“方法签名不同”指的是方法的参数顺序、参数类型和个数不同(需要注意的是,方法返回类型不属于方法签名的一部分)。下面的代码演示了方法的重载:

class Program
{
    // 在Main方法中调用Person类中的静态方法
    static void Main(string[] args)
    {
        // 实例化Person对象
        Person p = new Person();

        // 通过不同的传入参数来区分调用哪一个方法重载
        p.Print("张三");
        p.Print(18);
        p.Print("张三",18);
    }
}

// 方法重载的例子
public class Person
{
    // 打印方法
    public void Print(string name)
    {
        // 输出传入的值
        Console.WriteLine("输入的值为:" + name);
    }

    // 不属于方法重载的例子
    // 下面的方法会出现编译错误
    public int Print(string name)
    {
        // 输出传入的值
        Console.WriteLine("输入的值为:" + name);
        return 1;
    }
    
    // 打印方法的方法重载
    // 此时是参数的类型不同
    public void Print(int age)
    {
        // 输出传入的值
        Console.WriteLine("输入的值为:" + age);
    }

    // 打印方法的方法重载
    // 此时参数的个数不同,该方法有两个参数
    public void Print(string name, int age)
    {
        // 输出传入的值
        Console.WriteLine("输入的值为:{0},{1}", name, age);
    }
}

以上代码给出了一个不是方法重载的例子。如果两个方法只有返回类型不同,这样的方法不能称为方法重载。此时,编译器认为它们是两个相同名称的方法,所以会产生一个编译错误,错误信息为:类型 "Person" 已定义了一个名为 "Print" 的参数类型相同的成员 。


4.3.4 构造函数

构造函数主要用来创建类的实例对象. 当我们通过类名创建一个对象(实例)时, 会调用类的构造函数来创建对象(实例), 在这个创建的过程中, 构造函数会为对象分配内存空间, 并初始化类的成员. 构造函数分为实力构造函数和静态构造函数两种.

  1. 实例构造函数

    用于创建和初始化类的实例. 使用new运算符创建对象的过程, 其实就是在调用实例构造函数, 来初始化类中所有实例成员, 下面代码为使用方法.

    class Person
    {
      private string name;
      
      //实例构造函数
      public Person()
      {
        name="Learning Hard";
      }
    }
    
    

    构造函数还具有以下特点.

    • 可以进程方法重载. 该特点可以通过不同方式完成类的实例化, 比如类在实例化的时候, 根据输入参数的不同调用不同的构造函数来进行实例化.
    • 如果没有为类显式地定义一个构造函数, 则C#编译器会自动生成一个函数体为空地默认无参地实例构造函数.
    • 可以通过使用修饰符对实例构造函数指定访问级别, 比如public, protected和private.

    然而不是所有函数都可以成为实例构造函数, 必须满足以下两个条件.

    • 构造函数名称必须和类一样
    • 构造函数不允许由返回类型

    之前的代码定义的是无参的实例构造函数, 也可以定义有参数的实例构造函数, 通过传递的参数来对类中的成员进行初始化. 下面的代码演示了带参数构造函数的使用方法.

    class Person
    {
        private string name;
    
        public string Name
        {
            get
            {
                return name;
            }
        }
    
        public Person()
        {
            name = "Learning Hard";
        }
    
        public Person(string name)
        {
            // 使用this关键字来引用字段name,与参数name区分开来
            this.name = name; // this 代表当前类的实例对象
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            Person personWithoutPara = new Person();
            Person personWithPara = new Person("张三");
    
            Console.WriteLine(personWithoutPara.Name);
            Console.WriteLine(personWithPara.Name);
            Console.Read();
        }
    }
    

    效果:

    Learning Hard
    张三
    

    上面代码定义的都是公共构造函数(即用public关键字修饰的构造函数), 还有私有构造函数(使用private关键字修饰的构造函数).

    如果类只定义了一个或者多个私有构造函数, 而没有其他公共构造函数, 则其他类不能通过调用该类的私有构造函数来创建类的实例. 私有构造函数最典型的应用是单例设计模式.

    下面是单例设计模式中的一种, 也是私有构造函数的最典型应用的示例.

    // 下面实现也是设计模式中单例模式的实现
    class Person
    {
        private string name;
        public static Person person;
        public string Name
        {
            get { return this.name; }
        }
    
        // 私有构造函数,只能在类内部调用
        // 也就是说类的实例化只能在类定义时被实例化
        private Person()
        {
            Console.WriteLine("私有构造函数被调用");
            this.name = "Learning Hard";
        }
    
        // 静态方法,用于返回类的实例
        public static Person GetInstance()
        {
        if(person==null)    
            person = new Person();
            return person;
        return person; 
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            // 通过调用GetInstance()静态方法来返回一个Person的实例
            // 此时不能使用new运算符来创建实例
            // 即不能使用 Person person =new Person()的代码来创建实例
            Person person = Person.GetInstance();
    
            Console.WriteLine("类实例的Name属性为:{0}", person.Name);
            Console.Read();
        }
    }
    
    

    运行后得到结果:

    私有构造函数破调用
    类实例的Name属性为:Learning Hard
    
  2. 静态构造函数

    该函数用于初始化类中的静态成员, 在创建第一个实例或引用任何静态成员之前, CLR都将自动调用静态构造函数.
    下面代码演示了静态构造函数的使用方法.

    class Person
    {
        private static string name;
    
        // 静态构造函数,仅执行一次
        static Person()
        {
            Console.WriteLine("静态构造函数被调用了");
            name = "Learning Hard";
        }
    
        public static string Name
        {
            get { return name; }
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Person.Name);
            Console.WriteLine(Person.Name);
            Console.Read();
        }
    }
    

    运行结果:

静态构造函数被调用了 
Learning Hard 
Learning Hard
	在前面的代码中,两次调用了Person 对象的静态Name 属性,但只输出了一次 “静态构造函数被调用了”的语句,这足以说明静态构造函数与实		例构造函数的不同之处——静态构造函数只执行一次。与实例构造函数一样,要成为静态构造函数必须具有以下特点。
  • 不能使用任何访问修饰符
  • 不能带有任何参数
  • 只会执行一次
  • 不能直接被
  • 开发者无法在程序中控制执行静态构造函数的时机

4.3.5 析构函数

用于在类销毁之前释放类实例所使用的托管和非托管资源。对于C#应用程序所创建的大多数对象,可以依靠.NET Framework的垃圾回收器(GC)来隐式地执行内存管理任务。但若创建封装了非托管资源的对象,在应用程序使用完这些非托管资源之后,垃圾回收器将运行对象的析构函数(即Finalize 方法)来释放这些资源。

下面的代码演示了析构函数的定义。

class Person
{
    // 析构函数
    ~Person()
    {
        Console.WriteLine("析构函数被调用了");
    }
}

该析构函数隐式地调用了基类Object 的Finalize 方法,上面代码中的析构函数将被隐式地转换为如下代码:

protected override void Finalize()
{
    try
    {
       Console.WriteLine("析构函数被调用了");
    }
    finally
    {
        // 调用Object的Finalize方法
        base.Finalize();
    }
}

以上代码中,try…finally… 是C#的异常处理机制,即在代码发生错误时进行相应的处理。其中,把可能发生错误的代码放在try 语句块里,通过catch 语句块来对发生的错误进行处理,最后在finally 语句块中对代码用到的对象进行释放并清理其他资源.

有关异常处理, 可以参考[MSDN](https://learn.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2008/ms173160(v=vs.90)

在定义析构函数时需要注意以下几点:

  • 不能在结构体中定义析构函数,只能对类使用析构函数;
  • 一个类只能有一个析构函数;
  • 无法继承或重载析构函数;
  • 无法显式地调用析构函数,析构函数是由垃圾回收器自动调用的;
  • 析构函数既没有修饰符也没有参数。

4.3.6 索引器

当一个类包含数组成员时, 索引器的使用将大幅度简化对类中数组成员的访问. 它的定义类似于属性, 也具有get和set访问器.

具体代码如下:

[修饰符] 数据类型 this[索引类型 index]
{
    get{ // 返回类中数组某个元素}
    set {// 对类中数组元素赋值 }
}

其中, 数据类型指的是类中要存取的数据类型; 索引类型标识该索引器使用哪一个类型的索引来存取数组元素, 可以是整形, 也可以是字符串类型; this则表示所操作的是类对象的数组成员, 可以简单地把它理解为索引器的名字(注意, 索引器不能自定义名称).

下面代码演示了索引器的定义:

class Person
{
    // 可容纳10个整数的整数数组
    private int[] intarray = new int[10];

    // 索引器的定义
    public int this[int index]
    {
        get
        {
            return intarray[index];
        }
        set
        {
            intarray[index] = value;
        }
    }
}

上述代码让我们可以像访问数组一样访问类中的数组成员.

使用方式如下:

class Program
{
    static void Main(string[] args)
    {
        Person person = new Person();

        // 通过索引器对类中的数组进行复制
        person[0] = 1;
        person[1] = 2;
        person[2] = 3;

        Console.WriteLine(person[0]);
        Console.WriteLine(person[1]);
        Console.WriteLine(person[2]);
        Console.Read();
    }
}

以上代码可以看出, 索引器也是一种针对私有字段进行访问的方式, 但此时的私有字段是数组类型, 而属性一般只针对简单数据类型的私有字段进行访问. 可以理解为: 索引器是属性概念的延伸.

4.4 类实例化

前面说了可以定义的成员, 包括字段, 属性, 构造函数, 实例方法和析构函数等. 要访问这些成员, 必须通过类的实例对象来完成. 而要得到一个类的实例对象, 就必须声明一个该类类型的变量, 然后利用new运算符后跟类的实力构造函数来完成实例化. 类的实例对象是对类的具体化.

下面代码中, Human类只是概念中的人类, 实例化以后就产生了具体的某个人, 这里就实例化了两个具体的对象: 张三和李四.

class Program
{
    static void Main(string[] args)
    {
        // 实例化两个类对象
        Person person1 = new Person("张三", true);
        Person person2 = new Person("李四", false);
    }
}

class Person
{
    private string name;
    private bool sex;

    public string Name
    {
        get;
        set;
    }
    public bool Sex
    {
        get;
        set;
    }

    // 带两个参数的实例构造函数
    public Person(string name,bool sex)
    {
        this.name = name;
        this.sex = sex;
    }
}

对于类的实例化, 需要注意: 只有包含实例构造函数的类才能被实例化, 静态类中是不能定义实例构造函数的.


4.5 类与结构体的区别

由于类与结构体在语法和使用上都非常相似, 下面进行一些比较.

  • 关键词不同, 类是class, 结构体是struct

  • 结构体中不可对声明字段进行初始化, 类可以. 如下代码会报错: "结构体与类区别.Point,x”:结构中不能有实例字段初始值

    struct Point
    {
      private int x = 1;
      private int y;
    }
    
  • 如果没有为类显式地定义构造函数, C#编译器会自动生成一个无参数的隐式实例构造函数; 一旦我们为类显式地定义了一个构造函数, C#编译器就不会再自动生成隐式构造函数了. 结构体不同, 无论是否显式地定义了构造函数, 隐式构造函数都是一直存在的. 因此结构体中不能显式地定义无参数地构造函数.

  • 结构体地构造函数中, 必须为所有字段赋值, 否则如下所示会报错:在控制返回调用方之前,字段"结构体与类区别.Poit.y”"必须被完全赋值

    struct Point
    {
      private int x;
      private int y;
      public Point(int x)
      {
        this.x = x;
      }
    }
    
  • 创建结构体对象可以不使用new关键字, 但此时结构体对象中的字段是没有初始值的; 而类必须使用new关键字来创建对象.

  • 结构体不能继承结构或者类, 但可以实现接口; 而类可以继承类但不能继承结构, 也可以实现接口.

  • 类是引用类型, 而结构体是值类型.

  • 结构体不能定义析构函数, 而类可以有析构函数.

  • 结构体不能用abstract和sealed关键字修饰, 而类可以.


4.6 归纳总结

本章主要介绍了C#中类和类成员的定义,其中类成员的定义又包括对字段、属性、方法、构造函数和析构函数的定义。最后详细地比较了类与结构体的区别.