C#学习记录-类(Class)

在学习C#的过程中,"类(Class)"是一个绕不开的核心知识点。无论是控制台程序、桌面程序、还是Web应用开发,类几乎贯穿了整个C#的编程过程。可以说,掌握类的定义与使用,是进入面向对象编程的重要一步。

接下来我会从类的基本概念、组成部分、访问修饰符、构造函数、静态成员、继承、多态等方面进行介绍,会有相关具体的案例程序,建议跟着敲一遍学习理解。

一、什么是类

在C#中,类是对象的模板 ,它定义了某类事物具有哪些属性行为。例如:我们可以把一个学生看成一个类:

  • 学生有姓名、年龄等属性
  • 学生有自我介绍、学习等行为

当我们根据这个类创建具体对象时,就得到了一个真实的学生实例。

1.1类与对象的关系

可以把类和对象理解为:

:图纸、模板

对象:按照图纸造出来的具体实体

比如:

Student是一个类

student1、student2是由Student创建出来的对象

二、定义一个最简单的类

下面是一个最基础的Student类示例:

cs 复制代码
using System;
public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
    public void Introduce()
    {
        Console.WriteLine($"Hello, my name is {Name} and I am {Age} years old.");
    }

}
public class Program
{
    public static void Main(string[] args)
    {
        Student student = new Student { Name = "Alice", Age = 20 };
        student.Introduce();
    }
}

运行结果

在这段代码中:Student是类,Name和Age是属性。Introduce()是方法,student是创建出来的对象。这段代码展示了类最基本的使用方式。

三、类的组成部分

一个完成的类通常是由以下几个部分构成:字段属性方法、构造函数。在下面会分别进行说明。

3.1字段

字段用于在类中保存数据

cs 复制代码
public class Student
{
    public string name;
    public int age;
}

虽然字段可以直接存储数据,但在实际开发中,不建议把字段直接暴露为public。因为这样会导致外部可以随意修改数据,不利于封装和数据安全。因此,C#中更推荐使用属性来管理数据。

注意:C#类里的成员默认是私有的,这点跟C++不一样。

3.2属性

属性可以理解为对字段的一层封装。它既能像变量一样方便访问,又能在读取和赋值时加入逻辑控制 。从表面上看,属性的使用方式和字段很像,都是通过"对象.成员名"的方式进行访问,但是从本质上来说,属性并不是单纯的变量,而是一种提供受控访问的成员

简单来说:

字段更偏向于直接存储数据

属性更偏向于通过方法的方式访问数据

因此,在实际开发中,通常推荐使用属性而不是直接公开字段

3.2.1属性的基本语法

属性通常由两个访问器组成:

  • get:读取属性值
  • set:设置属性值

基本示例

cs 复制代码
public class Student
{
    private string name;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}

这里name是私有字段,Name是公开属性,外部可以通过Name访问数据,而不是直接访问name

get访问器

get用于返回属性值,也就是"读取数据"。

cs 复制代码
get {return name;}

当我们执行:

cs 复制代码
Console.WriteLine(student.Name);

实际上就是调用get。

set访问器

set用于给属性赋值,也就是写入数据

cs 复制代码
set { name = value; }

当我们执行:

cs 复制代码
student.Name = "张三";

实际上就是调用set。这里的value是C#自动提供的一个隐含参数,表示外部传入的新值。

例如:

cs 复制代码
student.Name = "李四";

那么在set中,value就等于李四。

3.2.2自动实现属性

如果属性不需要额外的逻辑,比如不需要校验、不需要格式处理,那么可以使用更简洁的写法:自动实现属性

cs 复制代码
public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
}

这种写法由编译器自动生成隐藏字段,因此代码更加简洁。

示例:

cs 复制代码
Student student = new Student();
student.Name = "王五";
student.Age = 20;

Console.WriteLine($"姓名:{student.Name},年龄:{student.Age}");

自动实现属性适用于简单的数据保存场景,也是实际开发中最常见的写法之一。

3.2.3带私有字段的属性

如果属性需要添加校验逻辑、格式控制或其他操作,就需要手动顶一个私有字段作为后备存储。

cs 复制代码
using System;

public class Student
{
    private int age;

    public string Name { get; set; }

    public int Age
    {
        get { return age; }
        set
        {
            if (value < 0 || value > 150)
            {
                Console.WriteLine("年龄输入不合法!");
            }
            else
            {
                age = value;
            }
        }
    }
}

测试代码

cs 复制代码
public class Program
{
    public static void Main()
    {
        Student student = new Student();
        student.Age = 20;
        Console.WriteLine(student.Age);

        student.Age = -5;
        Console.WriteLine(student.Age);
    }
}

输出结果

这种写法体现了属性的真正价值:不只是保存数据,还能控制数据。

3.2.4只读属性

有些数据只允许读取,不允许在类外随意修改,这时可以只保留get。

示例

cs 复制代码
public class Person
{
    public string IdCard { get; }

    public Person(string idCard)
    {
        IdCard = idCard;
    }
}

使用示例

cs 复制代码
Person person = new Person("123456789");
Console.WriteLine(person.IdCard);

这种属性在对象创建后就不能再随意修改,适合表示一些固定不变的数据,比如:

身份证号、学号、创建时间、唯一编号等

3.2.5访问器使用不同的访问级别

属性本身可以是public,但get和set也可以拥有不同的访问权限。

例如我们希望某个属性可以被外部读取,但只能在类内部修改:

cs 复制代码
public class Account
{
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        if (amount > 0)
        {
            Balance += amount;
        }
    }
}

使用示例

cs 复制代码
Account account = new Account();
account.Deposit(500);
Console.WriteLine(account.Balance);

// account.Balance = 1000;  // 这里会报错,因为 set 是 private

这种设计非常常见,优点是外部可以查看数据但不能直接篡改数据,必须要通过类提供的方法来修改,这样能更好的保护对象内部状态。

3.2.6init访问器:只允许初始化时赋值

在较新的C#版本中,还可以使用init访问器。它表示属性只能在对象初始化时赋值,之后不能再修改。

示例

cs 复制代码
public class Student
{
    public string Name { get; init; }
    public int Age { get; init; }
}

使用方式

cs 复制代码
Student student = new Student
{
    Name = "小明",
    Age = 20
};

Console.WriteLine($"{student.Name} - {student.Age}");

创建完后再修改

cs 复制代码
// student.Name = "小红";  // 报错

init的特点是比set更严格,适合初始化后不可改变的对象,常用于配置对象、数据模型对象。

3.2.7表达式属性

对于逻辑简单的属性,可以使用更简洁的写法,成为表达式体属性。

示例

cs 复制代码
public class Circle
{
    public double Radius { get; set; }

    public double Area => 3.14 * Radius * Radius;
}

这里的Area是一个只读计算属性,等价于:

cs 复制代码
public double Area
{
    get { return 3.14 * Radius * Radius; }
}

表达式体属性适合用于代码简单,返回值明确,不需要复杂逻辑的场景,这样写代码会更简洁,也更现代化。

3.2.8属性与字段的区别

可以参考下面这张表格:

|---------|------------|-----------|
| 对比项 | 字段 | 属性 |
| 本质 | 存储数据的变量 | 封装数据访问的成员 |
| 是否可控制访问 | 较弱 | 较强 |
| 是否能加逻辑 | 不方便 | 可以 |
| 使用建议 | 通常为private | 对外通常使用属性 |

建议写法:一般都是字段用于内部存储,属性对外暴露

cs 复制代码
public class Student
{
    private int age;   // 字段

    public int Age     // 属性---属性首字母要大写
    {
        get { return age; }
        set
        {
            if (value >= 0)
            {
                age = value;
            }
        }
    }
}

3.2.9属性的小案例

下面用一个商品类Product来综合展示属性的几种常见写法(建议跟着敲一遍,好好体会属性的各种写法)

cs 复制代码
using System;

public class Product
{
    private decimal price;

    // 自动实现属性
    public string Name { get; set; }

    // 只读属性
    public string ProductCode { get; }

    // 带校验逻辑的属性
    public decimal Price
    {
        get { return price; }
        set
        {
            if (value < 0)
            {
                Console.WriteLine("商品价格不能小于 0!");
            }
            else
            {
                price = value;
            }
        }
    }

    // 计算属性
    public string DisplayInfo => $"商品编号:{ProductCode},名称:{Name},价格:{Price} 元";

    public Product(string productCode, string name, decimal price)
    {
        ProductCode = productCode;
        Name = name;
        Price = price;
    }
}

public class Program
{
    public static void Main()
    {
        Product product = new Product("P1001", "机械键盘", 299);

        Console.WriteLine(product.DisplayInfo);

        product.Price = -50;   // 非法赋值
        Console.WriteLine(product.DisplayInfo);
    }
}

输出结果

3.3方法

方法是用于描述类或对象的行为,也就是能做什么。如果说属性表示对象有什么,那么方法就是表示对象能做什么。例如,学生可以学习,银行账户可以存款和取款。

3.3.1方法的基本概念

方法本质上是一段可以重复调用的代码,用来完成某个具体功能。再类中定义方法,可以让对象拥有行为,并提高代码的复用性和可读性。

基本语法

cs 复制代码
访问修饰符 返回值类型 方法名(参数列表)
{
    // 方法体
}

例如

cs 复制代码
public void SayHello()
{
    Console.WriteLine("你好,欢迎学习 C#!");
}

这个方法中,public表示访问修饰符,void表示没有返回值,SayHello是方法名,()表示没有参数,{}中是方法执行的代码

3.3.2无参无返回值方法

这是最简单的方法形式,适合执行某个动作,但不需要接收输入,也不需要返回结果。

示例

cs 复制代码
using System;

public class Student
{
    public void Introduce()
    {
        Console.WriteLine("大家好,我是一名学生。");
    }
}

public class Program
{
    public static void Main()
    {
        Student student = new Student();
        student.Introduce();
    }
}

输出结果

这种方法适合用于简单输出、提示信息、执行固定操作等场景。

3.3.3有参无返回值方法

如果方法执行时需要外部提供数据,就可以使用参数。

示例:学习某门课程

cs 复制代码
using System;

public class Student
{
    public string Name { get; set; }

    public void Study(string subject)
    {
        Console.WriteLine($"{Name}正在学习 {subject}。");
    }
}

public class Program
{
    public static void Main()
    {
        Student student = new Student();
        student.Name = "张三";
        student.Study("C#");
    }
}

输出结果

这里的subject就是方法参数,表示调用方法时传入的数据。

3.3.4无参有返回值方法

有些方法不需要接收参数,但需要返回一个结果

示例:获取问候语

cs 复制代码
using System;

public class Greeting
{
    public string GetMessage()
    {
        return "欢迎来到 C# 的世界!";
    }
}

public class Program
{
    public static void Main()
    {
        Greeting greeting = new Greeting();
        string message = greeting.GetMessage();

        Console.WriteLine(message);
    }
}

输出结果:这里return返回了一个字符串结果

3.3.5有参有返回值方法

这是最常见的一种方法形式,既能接收输入,又能返回处理结果。

示例:计算两个数的和

cs 复制代码
using System;

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

public class Program
{
    public static void Main()
    {
        Calculator calculator = new Calculator();
        int result = calculator.Add(3, 5);

        Console.WriteLine($"计算结果:{result}");
    }
}

输出结果:这种方法特别适合做数据处理、计算、查询等操作

3.3.6return 关键字

在有返回值的方法中,通常需要使用return把结果返回给调用者

例如:

cs 复制代码
public int Square(int number)
{
    return number * number;
}

如果方法返回类型不是void,那么通常必须返回一个符合类型要求的值。而如果方法返回类型是void,则表示不返回结果:

cs 复制代码
public void PrintMessage()
{
    Console.WriteLine("这是一条消息。");
}

3.3.7方法参数

参数是方法与外部交互的重要方式。通过参数,方法可以接收调用者传入的数据。

示例:

cs 复制代码
public void ShowInfo(string name, int age)
{
    Console.WriteLine($"姓名:{name},年龄:{age}");
}

调用方法时:

cs 复制代码
ShowInfo("李四", 20);

这里:string name和int age都时属于参数。

3.3.8方法重载

在同一个类中,允许定义多个同名方法,只要它们的参数列表不同即可,这叫做方法重载。

方法重载的判断依据主要是:参数的个数不同参数的类型不同参数的顺序不同

返回值类型不同,不能单独构成重载

示例:加法方法重载

cs 复制代码
using System;

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public double Add(double a, double b)
    {
        return a + b;
    }

    public int Add(int a, int b, int c)
    {
        return a + b + c;
    }
}

public class Program
{
    public static void Main()
    {
        Calculator calculator = new Calculator();

        Console.WriteLine(calculator.Add(2, 3));
        Console.WriteLine(calculator.Add(2.5, 3.1));
        Console.WriteLine(calculator.Add(1, 2, 3));
    }
}

输出结果

方法重载的好处是:同一个功能可以根据不同的输入方式灵活调用,提高代码的可读性。

3.3.9可选参数

在C#中,方法参数可以设置默认值,这样调用时可以不传这个参数。

示例:

cs 复制代码
using System;

public class MessageService
{
    public void SendMessage(string message, string receiver = "管理员")
    {
        Console.WriteLine($"向 {receiver} 发送消息:{message}");
    }
}

public class Program
{
    public static void Main()
    {
        MessageService service = new MessageService();

        service.SendMessage("系统运行正常");
        service.SendMessage("会议将在下午开始", "张老师");
    }
}

输出结果

可选参数的优点是简化调用方式,适合某些参数在大多数情况下都有固定默认值的场景。

3.3.10命名参数

调用方法时,可以通过"参数名:值"的方式明确指定参数,这叫做命名参数

示例

cs 复制代码
using System;

public class Student
{
    public void Register(string name, int age, string major)
    {
        Console.WriteLine($"姓名:{name},年龄:{age},专业:{major}");
    }
}

public class Program
{
    public static void Main()
    {
        Student student = new Student();
        student.Register(age: 20, major: "计算机科学", name: "王五");
    }
}

命名参数可以提高代码可读性,特别是在参数较多时很有帮助。

3.3.11ref、out和in参数

这是方法参数中进阶的内容,涉及到了值传递和引用传递的概念,不理解可以先往后看,了解了值传递和引用再看就能理解了。

ref参数

ref表示按引用传递参数,调用方法前,变量必须先初始化。方法内部对参数的修改会影响到外部变量。

示例

cs 复制代码
using System;

public class Calculator
{
    public void AddTen(ref int number)
    {
        number += 10;
    }
}

public class Program
{
    public static void Main()
    {
        Calculator calculator = new Calculator();
        int value = 5;

        calculator.AddTen(ref value);
        Console.WriteLine(value);
    }
}

输出结果

out参数

out也表示按引用传递,但调用前不要求变量已赋值。方法内部必须对out参数赋值。

示例

cs 复制代码
using System;

public class Division
{
    public bool TryDivide(int a, int b, out int result)
    {
        if (b == 0)
        {
            result = 0;
            return false;
        }

        result = a / b;
        return true;
    }
}

public class Program
{
    public static void Main()
    {
        Division division = new Division();

        if (division.TryDivide(10, 2, out int result))
        {
            Console.WriteLine($"结果是:{result}");
        }
        else
        {
            Console.WriteLine("除数不能为 0");
        }
    }
}

输出结果

out常用于方法返回多个结果的场景。

in参数

in表示按引用传递,但方法内部不能修改参数,它适合传递较大的数据结构,同时又不希望被修改。

示例

cs 复制代码
public void PrintValue(in int number)
{
    Console.WriteLine(number);
}

3.3.12实例方法与静态方法

方法可以分为实例方法静态方法

实例方法属于对象,必须通过对象来调用。

cs 复制代码
public class Student
{
    public string Name { get; set; }

    public void SayHello()
    {
        Console.WriteLine($"你好,我是{Name}");
    }
}

调用方式

cs 复制代码
Student student = new Student { Name = "小明" };
student.SayHello();

实例方法通常用于操作对象自己的数据。

静态方法

静态方法属于类本身,不依赖具体对象。

cs 复制代码
using System;

public class MathTool
{
    public static int Multiply(int a, int b)
    {
        return a * b;
    }
}

public class Program
{
    public static void Main()
    {
        int result = MathTool.Multiply(3, 4);
        Console.WriteLine(result);
    }
}

输出结果

静态方法适合处理哪些不依赖于对象状态的公共功能。

3.3.13表达式体方法

对于逻辑非常简单的方法,可以使用表达式体语法,让代码更加简洁。

示例

cs 复制代码
public class Calculator
{
    public int Square(int x) => x * x;
}

等价于

cs 复制代码
public class Calculator
{
    public int Square(int x)
    {
        return x * x;
    }
}

3.3.14方法中的访问修饰符

和属性一样,方法也可以通过访问修饰符控制可见范围。常见的修饰符是:

public:任何地方都可以访问

private:只能在当前类内部访问

protected:只能在当前类和子类中访问

internal:只能在当前程序集内访问

示例

cs 复制代码
using System;

public class BankAccount
{
    public string AccountName { get; set; }

    public void ShowInfo()
    {
        Console.WriteLine($"账户名:{AccountName}");
        LogOperation();
    }

    private void LogOperation()
    {
        Console.WriteLine("记录日志:调用了 ShowInfo 方法。");
    }
}

在这里,ShowInfo()是公开方法,外部可以调用。LogOperation()是私有方法,只能在内部调用。这样设计可以隐藏实现细节,让类的结果更加清晰。具体等下也会在类的继承、多态再做讨论。

3.3.15局部函数

C#还支持在方法内部定义方法,这种方法叫做局部函数

示例

cs 复制代码
using System;

public class Calculator
{
    public int Calculate()
    {
        int Add(int a, int b)
        {
            return a + b;
        }

        return Add(3, 5);
    }
}

局部函数适合哪些只在方法内部使用的小逻辑。

3.3.16一个完整的方法案例:银行账户类

同样为了让你更清晰的体会C#类方法的使用,提供了一个银行账户类:

包含账户名、余额、存款方法、取款方法、显示账户信息的方法

cs 复制代码
using System;

public class BankAccount
{
    public string AccountName { get; set; }
    public decimal Balance { get; private set; }

    public BankAccount(string accountName, decimal balance)
    {
        AccountName = accountName;
        Balance = balance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("存款金额必须大于 0。");
            return;
        }

        Balance += amount;
        Console.WriteLine($"存款成功,金额:{amount},当前余额:{Balance}");
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
        {
            Console.WriteLine("取款金额必须大于 0。");
            return;
        }

        if (amount > Balance)
        {
            Console.WriteLine("余额不足,取款失败。");
            return;
        }

        Balance -= amount;
        Console.WriteLine($"取款成功,金额:{amount},当前余额:{Balance}");
    }

    public void ShowInfo()
    {
        Console.WriteLine($"账户名:{AccountName},余额:{Balance}");
    }
}

public class Program
{
    public static void Main()
    {
        BankAccount account = new BankAccount("张三", 1000);

        account.ShowInfo();
        account.Deposit(500);
        account.Withdraw(300);
        account.Withdraw(2000);
        account.ShowInfo();
    }
}

输出结果

3.4构造函数

构造函数是类中的一种特殊方法,用于在创建对象时完成初始化工作。当我们使用new关键字创建对象时,构造函数会被自动调用。构造函数的主要作用是:让对象在一开始就处在一个合理、可用的状态

例如,一个学生对象在创建时,通常就应该有姓名和年龄。一个银行账户对象在创建时,通常就应该有账户名和初始余额。这些初始化工作就非常适合放在构造函数中完成。

3.4.1构造函数的基本特点

  1. 构造函数与普通方法不同,它有以下几个明显特征。
  2. 名称必须与类名相同
  3. 没有返回值类型,连void也不能写
  4. 创建对象时自动调用
  5. 主要用于初始化对象

示例

cs 复制代码
using System;

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Student()
    {
        Name = "未命名";
        Age = 18;
    }

    public void ShowInfo()
    {
        Console.WriteLine($"姓名:{Name},年龄:{Age}");
    }
}

public class Program
{
    public static void Main()
    {
        Student student = new Student();
        student.ShowInfo();
    }
}

输出结果

这里的Student()就是构造函数。当执行new Student()时,它会自动给对象的属性赋初始值。

3.4.2无参构造函数

无参构造函数指的是没有参数的构造函数,常用于给对象设置默认值。

示例

cs 复制代码
public class Car
{
    public string Brand { get; set; }
    public string Color { get; set; }

    public Car()
    {
        Brand = "未知品牌";
        Color = "白色";
    }
}

使用实例

cs 复制代码
Car car = new Car();
Console.WriteLine($"{car.Brand} - {car.Color}");

输出结果

无参构造就适合给对象设置默认状态,简化对象创建过程,方便某些框架进行实例化。

3.4.3有参构造函数

有参构造函数可以在创建对象时直接传入必要的数据,让对象一开始就具备完整信息。

示例

cs 复制代码
using System;

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Student(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public void ShowInfo()
    {
        Console.WriteLine($"姓名:{Name},年龄:{Age}");
    }
}

public class Program
{
    public static void Main()
    {
        Student student = new Student("张三", 20);
        student.ShowInfo();
    }
}

输出结果

这种方式比先创建对象,再逐个赋值更直接,也更容易保证数据完整性。

3.4.4构造函数重载

一个类可以定义多个构造函数,只要它们的参数列表不同即可,这叫做构造函数重载

示例

cs 复制代码
using System;

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Student()
    {
        Name = "未命名";
        Age = 18;
    }

    public Student(string name)
    {
        Name = name;
        Age = 18;
    }

    public Student(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

public class Program
{
    public static void Main()
    {
        Student s1 = new Student();
        Student s2 = new Student("李四");
        Student s3 = new Student("王五", 21);

        Console.WriteLine($"{s1.Name} - {s1.Age}");
        Console.WriteLine($"{s2.Name} - {s2.Age}");
        Console.WriteLine($"{s3.Name} - {s3.Age}");
    }
}

输出结果

构造函数重载的好处是提供了多种创建对象的方式,提高了灵活性,可以满足不同场景下的初始化需求。

3.4.5使用this调用其他构造函数

当一个类中有多个构造函数时,可能会出现重复代码。这是可以通过this(...)在一个构造函数中调用另一个构造函数,从而减少重复。

示例

cs 复制代码
using System;

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Student() : this("未命名", 18)
    {
    }

    public Student(string name) : this(name, 18)
    {
    }

    public Student(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

public class Program
{
    public static void Main()
    {
        Student s1 = new Student();
        Student s2 = new Student("小明");
        Student s3 = new Student("小红", 20);

        Console.WriteLine($"{s1.Name} - {s1.Age}");
        Console.WriteLine($"{s2.Name} - {s2.Age}");
        Console.WriteLine($"{s3.Name} - {s3.Age}");
    }
}

输出结果

3.4.6默认构造函数

如果一个类中没有写任何构造函数,那么C#编译器会自动提供一个默认的无参构造函数。

例如:

cs 复制代码
public class Person
{
    public string Name { get; set; }
}

这个类虽然没有显式写构造函数,但仍然可以这样创建对象

cs 复制代码
Person person = new Person();

不过需要注意的是:一旦你自己定义了任意构造函数,编译器就不会再自动生成默认无参构造函数。

例如:

cs 复制代码
public class Person
{
    public string Name { get; set; }

    public Person(string name)
    {
        Name = name;
    }
}

这是如果你写:

cs 复制代码
Person p = new Person(); // 会报错

就会出错,因为无参构造函数已经不存在了。如果你既需要无参构造,也需要有参构造,就必须自己都写出来。

3.4.7构造函数与对象初始化器

除了使用构造函数,C#还支持对象初始化器来给属性赋值。

示例

cs 复制代码
public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
}

创建对象时可以这样写

cs 复制代码
Student student = new Student
{
    Name = "赵六",
    Age = 22
};

这和下面这种方式相比更简洁

cs 复制代码
Student student = new Student();
student.Name = "赵六";
student.Age = 22;

构造函数和对象初始化器的区别

构造函数更适合初始化对象创建时必须具备的数据,对象初始化器更适合可选属性的赋值。如果某些数据是对象必须有的,通常建议放到构造函数里,如果只是附加设置,可以放到对象初始化器中。

3.4.8构造函数中也可以加入校验逻辑

构造函数不仅能赋值,还可以检查传入参数是否合法

示例

cs 复制代码
using System;

public class Book
{
    public string Title { get; set; }
    public double Price { get; set; }

    public Book(string title, double price)
    {
        if (string.IsNullOrWhiteSpace(title))
        {
            throw new ArgumentException("书名不能为空");
        }

        if (price < 0)
        {
            throw new ArgumentException("价格不能为负数");
        }

        Title = title;
        Price = price;
    }
}

使用示例

cs 复制代码
Book book = new Book("C# 入门教程", 58.8);
Console.WriteLine($"{book.Title} - {book.Price}");

这种写法的意义在于,从对象创建的第一刻开始,就保证它是合法的。这也是构造函数非常重要的价值之一。

3.4.9静态构造函数

除了普通构造函数,C#还有一种特殊的构造函数,叫做静态构造函数,用于初始化类本身的静态成员。它的特点是使用static修饰,没有访问修饰符,没有参数,一个类中最多只能有一个静态构造函数,再类第一次使用前自动执行一次,不能手动调用。

示例

cs 复制代码
using System;

public class School
{
    public static string SchoolName { get; set; }

    static School()
    {
        SchoolName = "实验中学";
        Console.WriteLine("静态构造函数执行了。");
    }
}

public class Program
{
    public static void Main()
    {
        Console.WriteLine(School.SchoolName);
        Console.WriteLine(School.SchoolName);
    }
}

输出结果

从结果来看:静态构造函数只执行了一次,及时后面多次访问类,也不会重复执行。静态构造函数适合初始化静态字段,执行类级别的一次性准备工作。我们初学阶段了解即可,不需要过度深入。

3.4.10私有构造函数

构造函数还可以声明为private,表示外部不能直接创建该类的对象。

示例

cs 复制代码
public class Utility
{
    private Utility()
    {
    }

    public static void PrintMessage()
    {
        Console.WriteLine("这是一个工具类。");
    }
}

此时外部不能这样写

cs 复制代码
public class Utility
{
    private Utility()
    {
    }

    public static void PrintMessage()
    {
        Console.WriteLine("这是一个工具类。");
    }
}

只能通过静态成员使用该类

私有构造函数的常见用途是防止类被实例化,配合单例模式使用,工具类只提供静态方法时使用。

3.4.11构造函数与普通方法的区别

具体可以参考下表

|------|-----------|-----------|
| 对比项 | 构造函数 | 普通方法 |
| 名称 | 必须与类名相同 | 可自定义 |
| 返回值 | 没有返回值类型 | 可以有,也可以没有 |
| 调用时机 | 创建对象时自动调用 | 需要手动调用 |
| 作用 | 初始化对象 | 完成功能操作 |

3.5this关键字

this表示当前对象本身,当成员变量和方法参数同名时,通常使用this来区分

示例

cs 复制代码
using System;

public class Student
{
    private string name;

    public Student(string name)
    {
        this.name = name;
    }

    public void ShowName()
    {
        Console.WriteLine(this.name);
    }
}

public class Program
{
    public static void Main()
    {
        Student student = new Student("小红");
        student.ShowName();
    }
}

输出结果

3.6继承

在面向对象编程中,继承、多态、抽象类是彼此紧密联系的知识点。它们共同帮助我们建立更清晰的类结构,减少重复代码,并让程序具有更好的拓展性。可以用一句话分别概括他们之间的关系。

  • 继承:让子类复用父类的成员
  • 抽象类:让父类只描述应该有什么,而不一定提供完整实现
  • 多态:让相同的调用方式,在不同对象上产生不同效果

3.6.1继承的基本概念

继承是面向对象的三大特性之一。它允许我们基于已有类创建新类,新类会自动获得原有类中的部分成员。

简单来说:

如果多个类具有共同特征,就可以把这些共同内容提取到一个父类中,再让其他类继承它。

例如:学生和老师都属于人,猫和狗都属于动物。这些关系都符合**"是一个"(is-a)**的特点,因此适合使用继承。

3.6.2为什么要使用继承

如果没有继承,不同类中很容易出现重复代码。

不适用继承的写法

cs 复制代码
public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void ShowInfo()
    {
        Console.WriteLine($"姓名:{Name},年龄:{Age}");
    }
}

public class Teacher
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void ShowInfo()
    {
        Console.WriteLine($"姓名:{Name},年龄:{Age}");
    }
}

这里的Student和Teacher的很多内容是重复的。

使用继承后的写法

cs 复制代码
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void ShowInfo()
    {
        Console.WriteLine($"姓名:{Name},年龄:{Age}");
    }
}

public class Student : Person
{
}

public class Teacher : Person
{
}

这样,公共内容统一写在Person中即可。

3.6.3继承的基本语法

在C#中,使用:表示继承。

cs 复制代码
class 子类名 : 父类名
{
}

例如:

cs 复制代码
public class Animal
{
    public string Name { get; set; }

    public void Eat()
    {
        Console.WriteLine($"{Name} 正在吃东西。");
    }
}

public class Dog : Animal
{
}

这里:Animal是父类,也叫做基类。Dog是子类,也叫做派生类。Dog继承Animal后,就可以直接使用Name和Eat()。

3.6.4子类可以继承什么

子类通常可以继承父类中的字段、属性、方法、事件、受保护成员,但是否能访问,还要看访问修饰符。

常见的情况:

  • public:子类可以访问,类外也可以访问
  • protected:子类可以访问,类外不能直接访问
  • private:只有父类自己能访问,子类不能直接访问

3.6.5子类拓展父类功能

继承的意义不仅在于复用,还在于拓展。

示例

cs 复制代码
using System;

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void ShowInfo()
    {
        Console.WriteLine($"姓名:{Name},年龄:{Age}");
    }
}

public class Student : Person
{
    public string StudentId { get; set; }

    public void Study()
    {
        Console.WriteLine($"{Name} 正在学习。");
    }
}

使用实例

cs 复制代码
Student student = new Student();
student.Name = "张三";
student.Age = 20;
student.StudentId = "S001";

student.ShowInfo();
student.Study();

这里的Student继承了Person的公共成员,又定义了自己的StudentId和Study(),这就是典型的保留共享,拓展个性。

3.6.6protected访问修饰符

在继承中,protected非常常见。

它表示:该成员只能在当前类和子类中访问,不能在类外直接访问。

示例

cs 复制代码
using System;

public class Animal
{
    protected string name;

    public void SetName(string name)
    {
        this.name = name;
    }
}

public class Cat : Animal
{
    public void Meow()
    {
        Console.WriteLine($"{name} 正在喵喵叫。");
    }
}

使用示例

cs 复制代码
Cat cat = new Cat();
cat.SetName("小白");
cat.Meow();

这里的name不能在类外直接访问,但子类Cat可以使用它。

3.6.7base关键字

在子类中,可以通过base访问父类成员,它又两个常见的用途,一个是调用父类的方法或属性,另一个是调用父类构造函数。
调用父类成员

cs 复制代码
using System;

public class Person
{
    public string Name { get; set; }

    public void ShowInfo()
    {
        Console.WriteLine($"姓名:{Name}");
    }
}

public class Student : Person
{
    public void ShowStudentInfo()
    {
        base.ShowInfo();
        Console.WriteLine("这是一个学生对象。");
    }
}

调用父类构造函数

cs 复制代码
using System;

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

public class Student : Person
{
    public string StudentId { get; set; }

    public Student(string name, int age, string studentId) : base(name, age)
    {
        StudentId = studentId;
    }
}

这里的:base(name,age)表示先调用父类构造函数,完成父类部分的初始化。

3.6.8继承与构造函数的关系

创建子类对象时,会先执行父类构造函数,再执行子类构造函数。

示例

cs 复制代码
using System;

public class Person
{
    public Person()
    {
        Console.WriteLine("父类构造函数执行了。");
    }
}

public class Student : Person
{
    public Student()
    {
        Console.WriteLine("子类构造函数执行了。");
    }
}

public class Program
{
    public static void Main()
    {
        Student student = new Student();
    }
}

输出结果

3.7抽象类

有些父类只是一个更高层次的概念,本身并不适合直接创建对象。

例如,动物时一个抽象概念,图形是一个抽象概念。这些时候就可以使用抽象类。

3.7.1抽象类的概念

抽象类使用abstract关键字修饰。它的主要作用是作为其他类的基类,描述一组共同特征。

特点是不能直接实例化可以包含普通成员可以包含抽象方法抽象方法必须由非抽象子类实现

3.7.2为什么需要抽象类

普通父类适合表示可以直接存在的对象,例如Person、Book等,但如果父类只是一个规则模板或概念模型,就更适合使用抽象类。

但如果父类只是一个规则模板或概念模型,就更适合使用抽象类。

例如:所有动物都能发出声音,但是不同动物的声音不同。所有图形都可以计算面积,但公式不同。所有员工都需要计算工资,但算法不同。

这种情况下,父类很难给出统一完整的实现,于是就额可以把它定义为抽象类。

3.7.3抽象类的基本语法

cs 复制代码
public abstract class 类名
{
}

示例

cs 复制代码
public abstract class Animal
{
    public string Name { get; set; }
}

这里Animal是抽象类,不能直接这样创建对象。

cs 复制代码
// Animal animal = new Animal(); // 错误

但是可以被子类继承。

3.7.4抽象方法

抽象类中可以定义抽象方法,抽象方法只有声明,没有方法体,用abstract修饰。

cs 复制代码
public abstract void 方法名();

抽象方法的意义是:父类只规定你必须有这个行为,但具体怎么实现,由子类决定。

3.7.5抽象类示例:动物发声

cs 复制代码
using System;

public abstract class Animal
{
    public string Name { get; set; }

    public abstract void MakeSound();

    public void Eat()
    {
        Console.WriteLine($"{Name} 正在吃东西。");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 汪汪叫。");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 喵喵叫。");
    }
}

public class Program
{
    public static void Main()
    {
        Dog dog = new Dog { Name = "大黄" };
        Cat cat = new Cat { Name = "小白" };

        dog.MakeSound();
        dog.Eat();

        cat.MakeSound();
        cat.Eat();
    }
}

输出结果

这个例子中,Animal是抽象类,MakeSound()是抽象方法,Eat()是普通方法,Dog和Cat必须实现MakeSound()

3.7.6抽象类和普通类的区别

|-----------|--------|--------|
| 对比项 | 普通类 | 抽象类 |
| 能否实例化 | 可以 | 不可以 |
| 是否能包含普通方法 | 可以 | 可以 |
| 是否能包含抽象方法 | 不可以 | 可以 |
| 主要用途 | 创建具体对象 | 作为基类模板 |

你可以简单理解为,普通类更偏向于直接使用,抽象类更偏向于提供规范和基础能力。

3.8多态

再学习了继承和抽象类之后,就可以进一步理解多态。多态是面向对象三大特性之一。它表示:同一个父类引用,在面对不同子类对象时,可以表现出不同的行为。简单来说就是,看起来调用的是同一个方法,实际执行的却可能是不同子类中的实现。这就是同一接口,多种表现。

3.8.1多态的实现前提

在C#中,常见的运行时多态通常需要满足以下条件:

  1. 存在继承关系
  2. 父类方法允许被重写(virtual或abstract)
  3. 子类重写该方法(override)
  4. 使用父类引用指向子类对象

3.8.2virtual和override

如果父类中的方法提供了一个默认实现,但运行子类重新定义,就可以使用virtual。

示例

cs 复制代码
using System;

public class Animal
{
    public string Name { get; set; }

    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} 发出声音。");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 汪汪叫。");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 喵喵叫。");
    }
}

这里父类方法使用virtual,子类方法使用override,这就形成了可重写关系。

3.8.3多态示例:父类引用指向子类对象

cs 复制代码
using System;

public class Animal
{
    public string Name { get; set; }

    public virtual void MakeSound()
    {
        Console.WriteLine($"{Name} 发出声音。");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 汪汪叫。");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine($"{Name} 喵喵叫。");
    }
}

public class Program
{
    public static void Main()
    {
        Animal a1 = new Dog { Name = "小黑" };
        Animal a2 = new Cat { Name = "小白" };

        a1.MakeSound();
        a2.MakeSound();
    }
}

输出结果

虽然a1和a2的类型都写成了Animal,但运行时会根据实际对象类型调用对应的重写方法。这就是多态最经典的表现。

3.8.4抽象类与多态的结合

实际上,抽象类和多态经常一起出现。原因是抽象类定义统一规范,子类提供具体实现,父类引用统一调用,形成多态效果。

例如前面的Animal,抽象类规定所有动物都必须实现MakeSound(),不同动物分别实现自己的叫声,程序在处理时只关心它是Animal,不必关系它到底是狗、猫还是鸟,这就是面向对象设计中非常典型的一种结构。

3.8.5完整的继承抽象多态案例

下面设计了一个员工工资系统:要求所有员工都有姓名,不同员工的工资计算方式不同,普通员工按固定工资计算,经理公司=基本工资+奖金,程序可以统一处理所有员工。

代码实现

cs 复制代码
using System;

public abstract class Employee
{
    public string Name { get; set; }

    public Employee(string name)
    {
        Name = name;
    }

    public abstract decimal CalculateSalary();

    public void ShowInfo()
    {
        Console.WriteLine($"员工:{Name},工资:{CalculateSalary()}");
    }
}

public class NormalEmployee : Employee
{
    public decimal BaseSalary { get; set; }

    public NormalEmployee(string name, decimal baseSalary) : base(name)
    {
        BaseSalary = baseSalary;
    }

    public override decimal CalculateSalary()
    {
        return BaseSalary;
    }
}

public class Manager : Employee
{
    public decimal BaseSalary { get; set; }
    public decimal Bonus { get; set; }

    public Manager(string name, decimal baseSalary, decimal bonus) : base(name)
    {
        BaseSalary = baseSalary;
        Bonus = bonus;
    }

    public override decimal CalculateSalary()
    {
        return BaseSalary + Bonus;
    }
}

public class Program
{
    public static void Main()
    {
        Employee[] employees =
        {
            new NormalEmployee("张三", 5000),
            new Manager("李四", 8000, 3000)
        };

        foreach (Employee employee in employees)
        {
            employee.ShowInfo();
        }
    }
}

输出结果

可以试着自己分析,体会继承,多态,抽象类的实际用法。

四、结语

类是C#编程中最重要的知识点之一,也是后续学习接口、泛型、集合、委托、LINQ、ASP.NET等内容的基础。对于初学者来说,学习类不仅仅是记住语法,更重要的是理解它背后的设计思想:封装、继承、多态。

五、参考资料

以下是微软官方提供的 C# 学习资料,适合进一步阅读和查阅:

  1. Microsoft Learn - C# 文档首页
    https://learn.microsoft.com/zh-cn/dotnet/csharp/

  2. Microsoft Learn - C# 编程指南
    https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/

  3. Microsoft Learn - C# 中的类和对象
    https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/tutorials/classes

  4. Microsoft Learn - 属性(Properties)
    https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/properties

  5. Microsoft Learn - 构造函数(Constructors)
    https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/constructors

  6. Microsoft Learn - 继承(Inheritance)
    https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/object-oriented/inheritance

  7. Microsoft Learn - 多态(Polymorphism)
    https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/object-oriented/polymorphism

相关推荐
AsDuang2 小时前
Python 3.12 MagicMethods - 55 - __irshift__
开发语言·python
共享家95272 小时前
Java入门(多态)
java·开发语言
机器视觉知识推荐、就业指导2 小时前
拆 Qt,为什么要先引入libmodbus?
开发语言·qt
2401_857865232 小时前
C++模块接口设计
开发语言·c++·算法
蓝莓星冰乐2 小时前
第一章:C语言概述与环境搭建
c语言·开发语言
add45a2 小时前
嵌入式C++低功耗设计
开发语言·c++·算法
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于Java的婚礼策划平台的设计与实现为例,包含答辩的问题和答案
java·开发语言
2401_874732532 小时前
C++中的状态模式
开发语言·c++·算法
red_redemption3 小时前
自由学习记录(132)
学习