类与接口
类的介绍
概念
类(Class)是面向对象编程(OOP)的基本构造块。它是一种自定义类型,用于封装数据和操作数据的方法。类定义了对象的属性和行为,是对象的蓝图。
就像建筑蓝图定义了一栋房子的结构、尺寸和设计一样,类定义了对象的数据结构(通过字段或属性)和行为(通过方法)。蓝图中的每个细节都对应于建筑的具体实现,类中的每个定义都对应于对象的具体状态和行为。
蓝图本身并不是一栋实际的房子,而是房子的抽象表示。同样,类是创建对象的模板。你也可以使用同一个类来创建多个具有相同属性和方法的对象。所有由同一个类创建的对象在结构上是相同的,但每个对象的属性(状态)可以有不同的值。
用途
类用于创建对象,它们提供了一种将数据和功能组织在一起的方式。通过类,可以实现代码的重用、封装、继承和多态等面向对象编程的特性。
类的使用
组成部分
类通常由以下几个部分组成:
- 字段(Fields):用于存储对象的状态,通常声明为
private
,通过属性访问。 - 属性(Properties):用于访问和修改对象的字段。提供对字段的访问控制,可以包含逻辑以保护字段。
- 方法(Methods):定义对象行为,可以是实例方法或静态方法。
- 构造函数(Constructors):初始化对象,可以重载,用于设置初始值。
- 析构函数(Destructors):用于清理对象。清理对象,不能重载,调用时间不确定。
类定义的一般形式
语法:
public class ClassName
{
// 字段
private int field;
// 属性
public int Property { get; set; }
// 方法
public void Method()
{
// 方法体
}
// 构造函数
public ClassName()
{
// 构造函数体
}
// 析构函数
~ClassName()
{
// 析构函数体
}
}
示例代码:
public class Person
{
// 字段
private string name;
private int age;
// 属性
public string Name
{
get { return name; }
set { name = value; }
}
public int Age
{
get { return age; }
set { age = value; }
}
// 构造函数
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
// 方法
public void Introduce()
{
Console.WriteLine($"Hello, my name is {name} and I am {age} years old.");
}
// 析构函数
~Person()
{
// 释放资源
}
}
类的实例化
类的实例化是指创建类的对象。可以使用 new
关键字来实例化类,使用点(.)运算符可以访问类的成员。
Person person = new Person("John", 30);
person.Introduce();
类的成员
字段(Fields)
字段是类的成员变量,用于存储对象的状态或数据。字段可以是任何数据类型,并且可以有不同的访问修饰符,如 public
、private
、protected
等。字段通常声明为 private
,以便通过属性来控制对它们的访问。
示例:
public class Car
{
// 私有字段
private string brand;
private int year;
// 公有字段
public string model;
}
属性(Properties)
属性提供了对类的字段的访问控制。属性包含 get
和 set
访问器,用于获取和设置字段的值。属性可以定义为只读、只写或可读写。
举个例子:
public class Car
{
private string brand;
private int year;
public string Brand
{
get { return brand; }
set { brand = value; }
}
public int Year
{
get { return year; }
set
{
if (value > 1885) // 汽车发明的年份
{
year = value;
}
}
}
}
class Program
{
static void Main(string[] args)
{
// 创建Car类的实例
Car myCar = new Car();
// 设置Brand属性
myCar.Brand = "Toyota";
// 尝试设置Year属性为一个有效值
myCar.Year = 2020;
// 打印Car实例的属性值
Console.WriteLine($"Brand: {myCar.Brand}, Year: {myCar.Year}");
// 尝试设置Year属性为一个无效值(汽车发明之前的年份)
myCar.Year = 1800;
// 再次打印Car实例的属性值,注意Year属性的值不会改变
Console.WriteLine($"Brand: {myCar.Brand}, Year: {myCar.Year}");
}
}
运行结果:
Brand: Toyota, Year: 2020
Brand: Toyota, Year: 2020
方法(Methods)
方法是类的行为,用于执行特定操作或功能。方法可以访问类的字段和属性,并可以接受参数和返回值。方法可以定义为实例方法或静态方法。
示例:
public class Car
{
private string brand;
private int year;
public Car(string brand, int year)
{
this.brand = brand;
this.year = year;
}
public void Drive() // 实例方法
{
Console.WriteLine($"{brand} is driving.");
}
public static void Honk() // 静态方法
{
Console.WriteLine("Car is honking.");
}
}
class Program
{
static void Main(string[] args)
{
// 使用类的静态方法
Car.Honk();
// 创建Car类的实例
Car myCar = new Car("Toyota", 2020);
// 使用类的实例方法
myCar.Drive();
}
}
运行结果如下:
Car is honking.
Toyota is driving.
提示
使用 static
关键字来修饰类的成员(字段、属性、方法等)时,这些成员属于类本身,而不是类的任何特定实例,此时可以在所有实例之间共享,并且可以在不创建类的实例的情况下访问。比如上述例子中,我们可以通过类名加 .
符号就可以访问 Honk
方法。
注意,静态成员不能访问类的非静态成员,无论有多少个类的对象被创建,只会有一个该静态成员的副本。
构造函数(Constructors)
在C#中,构造函数是一种特殊的方法,用于在创建类的新实例时初始化对象。构造函数的名称必须与类名相同,并且它不能有任何返回类型(甚至是 void
)。构造函数可以被重载,一个类可以有多个构造函数(只要它们的参数列表不同)。
要点:
- 自动调用:当创建类的实例时,构造函数会自动被调用。
- 初始化对象:构造函数主要用于初始化对象的状态,例如设置成员变量的初始值。
- 无返回类型:构造函数不定义返回类型,也不返回值。
构造函数有以下这些类型:
默认构造函数:如果你没有为类定义任何构造函数,C# 编译器将自动生成一个无参数的默认构造函数,它将所有成员初始化为其默认值(例如,值类型的成员被初始化为0,引用类型被初始化为null)。
参数化构造函数:你可以定义一个或多个构造函数,它们带有参数,这允许在创建对象时传递参数,用于初始化对象的状态。
静态构造函数:静态构造函数用于初始化类的静态成员。它没有访问修饰符,并且每个类只能有一个静态构造函数。静态构造函数不能直接被调用,它会在类被加载时自动执行。
私有构造函数:如果构造函数被声明为私有,那么外部代码不能直接创建该类的实例。这常用于实现单例模式或限制实例的创建。
示例
public class Car
{
private string brand;
private int year;
// 默认构造函数
public Car()
{
brand = "Unknown";
year = 2000;
}
// 参数化构造函数
public Car(string brand, int year)
{
this.brand = brand;
this.year = year;
}
}
析构函数(Destructors)
析构函数用于在对象被垃圾回收前执行清理操作。析构函数不能被直接调用,而是在垃圾回收器决定回收对象时自动执行。它主要用于释放对象持有的非托管资源,如文件句柄、数据库连接等。
要点:
- 自动调用:析构函数在垃圾回收器准备回收对象时自动调用。
- 无参数和访问修饰符:析构函数不能有参数,也不能有访问修饰符(如public或private),并且每个类只能有一个析构函数。
- 命名规则:析构函数的名称由波浪号(~)后跟类名组成。
提示
对于需要及时释放资源的情况,推荐实现IDisposable
接口并使用Dispose
方法来手动释放资源,而不是依赖析构函数。
示例:
public class Car
{
private string brand;
private int year;
public Car(string brand, int year)
{
this.brand = brand;
this.year = year;
}
// 析构函数
~Car()
{
// 执行清理操作
Console.WriteLine($"{brand} is being destroyed.");
}
}
抽象类
概念
C#中的抽象类是一个不能被实例化的类,旨在作为其他类的基类。
抽象类的主要作用是提供一个基础结构,可以包含抽象成员(即不包含实现的方法或属性),这些成员必须在派生类中实现。抽象类还可以包含已实现的方法或属性。通过抽象类,可以在不影响具体实现的情况下,定义类之间的共同接口和行为。
要点
不能实例化:抽象类不能被实例化,必须通过派生类来实现。
public abstract class Animal { public abstract void MakeSound(); } // 无法实例化 // Animal animal = new Animal(); // 错误
可以包含抽象成员:抽象类可以包含抽象方法、属性、索引器和事件,这些成员不包含实现,必须在派生类中实现。
public abstract class Animal { public abstract void MakeSound(); // 抽象方法 } public class Dog : Animal { public override void MakeSound() { Console.WriteLine("Woof!"); } }
可以包含已实现的成员:抽象类可以包含已实现的方法、属性、字段等。
public abstract class Animal { public abstract void MakeSound(); public void Sleep() { Console.WriteLine("Sleeping..."); } }
抽象方法只能在抽象类中:只有抽象类可以包含抽象方法,普通类不能包含抽象方法。
访问修饰符:抽象方法的访问修饰符可以是public、protected、internal,但不能是private,因为抽象方法必须由派生类实现。
示例
下面我们来看一个示例代码:
using System;
public abstract class Animal
{
public abstract void MakeSound();
public void Sleep()
{
Console.WriteLine("Sleeping...");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow!");
}
}
class Program
{
static void Main()
{
Animal dog = new Dog();
dog.MakeSound(); // 输出:Woof!
dog.Sleep(); // 输出:Sleeping...
Animal cat = new Cat();
cat.MakeSound(); // 输出:Meow!
cat.Sleep(); // 输出:Sleeping...
}
}
在这里,Animal
类是一个抽象类,定义了一个抽象方法MakeSound
和一个已实现的方法Sleep
。Dog
和Cat
类继承了Animal
类,并实现了MakeSound
方法。
接口
概念
提示
接口是定义一组方法和属性的规范,但不提供实现。
接口可以被类或结构实现,实现接口的类或结构必须提供接口中定义的所有方法和属性的具体实现。
接口使用 interface
关键字定义,接口中的方法和属性默认是公开的(public)且不包含访问修饰符。
public interface IAnimal
{
void Eat();
void Sleep();
}
实现接口
一个类或结构可以实现一个或多个接口。实现接口意味着提供接口中定义的所有方法和属性的具体实现。
public class Dog : IAnimal
{
public void Eat()
{
Console.WriteLine("Dog is eating.");
}
public void Sleep()
{
Console.WriteLine("Dog is sleeping.");
}
}
使用接口作为参数或返回类型
接口可以在方法的参数或返回类型中使用,这提高了方法的通用性和灵活性。
public void TakeCareOfAnimal(IAnimal animal)
{
animal.Eat();
animal.Sleep();
}
这个方法可以接受任何实现了 IAnimal
接口的对象作为参数。
接口中的属性
接口不仅可以包含方法,还可以包含属性。实现接口的类必须提供属性的具体实现。
public interface IPropertyExample
{
int MyProperty { get; set; }
}
public class PropertyImplementation : IPropertyExample
{
private int _myProperty;
public int MyProperty
{
get { return _myProperty; }
set { _myProperty = value; }
}
}
默认实现(C# 8.0+)
从 C# 8.0 开始,接口可以提供方法的默认实现。这允许在不破坏现有实现的情况下向接口添加新的方法。
public interface IDefaultImplementation
{
void MethodA() => Console.WriteLine("Default implementation of MethodA");
}