ZetCode

C# 面向对象编程

最后修改于 2023 年 7 月 5 日

本文介绍了 C# 中的面向对象编程。

有三种广泛使用的编程范例:过程式编程、函数式编程和面向对象编程。C# 支持过程式编程和面向对象编程。

OOP 定义

面向对象编程 (OOP) 是一种使用对象及其交互来设计应用程序和计算机程序的编程范例。

OOP 中有一些基本的编程概念

抽象是通过对适合问题的类进行建模来简化复杂的现实。多态是以不同的方式对不同的数据输入使用运算符或函数的过程。封装隐藏了类的实现细节,使其对其他对象不可见。继承是一种使用已经定义的类来形成新类的方法。

C# 对象

对象是 C# OOP 程序的基本构建块。对象是数据和方法的组合。数据和方法被称为对象的成员。在 OOP 程序中,我们创建对象。这些对象通过方法相互通信。每个对象都可以接收消息、发送消息和处理数据。

创建对象有两个步骤。首先,我们定义一个类。是对象的模板。它是一个蓝图,描述了该类的所有对象共享的状态和行为。一个类可以用来创建许多对象。在运行时从类创建的对象称为该特定类的实例

Program.cs
var b = new Being();
Console.WriteLine(b);

class Being {}

在我们的第一个例子中,我们创建一个简单的对象。

class Being {}

这是一个简单的类定义。模板的主体是空的。它没有任何数据或方法。

var b = new Being();

我们创建一个 Being 类的新实例。为此,我们使用 new 关键字。b 变量是已创建对象的句柄。

Console.WriteLine(b);

我们将对象打印到控制台以获得对象的一些基本描述。打印一个对象是什么意思?当我们打印一个对象时,实际上是调用它的 ToString 方法。但我们还没有定义任何方法。这是因为每个创建的对象都继承自基类 object。它有一些所有已创建对象共享的基本功能。其中之一就是 ToString 方法。

$ dotnet run
Being

C# 对象属性

对象属性是捆绑在类实例中的数据。对象属性被称为实例变量成员字段。实例变量是在类中定义的变量,类中的每个对象都有一个单独的副本。

Program.cs
var p1 = new Person();
p1.name = "Jane";

var p2 = new Person();
p2.name = "Beky";

Console.WriteLine(p1.name);
Console.WriteLine(p2.name);

class Person
{
    public string name;
}

在上面的 C# 代码中,我们有一个具有一个成员字段的 Person 类。

class Person
{
    public string name;
}

我们声明一个 name 成员字段。public 关键字指定该成员字段可以在类块外部访问。

var p1 = new Person();
p1.name = "Jane";

我们创建一个 Person 类的实例,并将 name 变量设置为“Jane”。我们使用点运算符来访问对象的属性。

var p2 = new Person();
p2.name = "Beky";

我们创建 Person 类的另一个实例。在这里,我们将变量设置为“Beky”。

Console.WriteLine(p1.name);
Console.WriteLine(p2.name);

我们将变量的内容打印到控制台。

$ dotnet run
Jane
Beky

Person 类的每个实例都有 name 成员字段的单独副本。

C# 方法

方法是在类体内部定义的函数。它们用于对对象的属性执行操作。方法为我们的程序带来模块化

方法在 OOP 范例的封装概念中至关重要。例如,我们的 AccessDatabase 类中可能有一个 Connect 方法。我们不需要知道 Connect 方法如何准确地连接到数据库。我们只需要知道它用于连接到数据库。这在编程中,特别是在大型应用程序中,对于划分职责至关重要。

对象对状态和行为进行分组,方法代表对象的行为部分。

Program.cs
var c = new Circle();
c.SetRadius(5);

Console.WriteLine(c.Area());

class Circle
{
    private int radius;

    public void SetRadius(int radius)
    {
        this.radius = radius;
    }

    public double Area()
    {
        return this.radius * this.radius * Math.PI;
    }
}

在代码示例中,我们有一个 Circle 类。我们定义了两个方法。

private int radius;

我们有一个成员字段。它是圆的半径。private 关键字是一个访问修饰符。它告诉我们变量对外界是受限的。如果我们想从外部修改这个变量,我们必须使用公开可用的 SetRadius 方法。这样我们就可以保护我们的数据。

public void SetRadius(int radius)
{
    this.radius = radius;
}

这是 SetRadius 方法。this 变量是一个特殊变量,我们用它来从方法访问成员字段。this.radius 是一个实例变量,而 radius 是一个局部变量,仅在 SetRadius 方法中有效。

var c = new Circle();
c.SetRadius(5);

我们创建一个 Circle 类的实例,并通过调用圆对象上的 SetRadius 方法来设置其半径。我们使用点运算符来调用该方法。

public double Area()
{
    return this.radius * this.radius * Math.PI;
}

Area 方法返回圆的面积。Math.PI 是一个内置常量。

$ dotnet run
78.5398163397448

C# 构造函数

构造函数是一种特殊的方法。当创建对象时会自动调用它。构造函数不返回值。构造函数的目的是初始化对象的状态。构造函数与类具有相同的名称。构造函数是方法,因此也可以重载。

构造函数不能被继承。它们按照继承顺序调用。如果我们没有为类编写任何构造函数,C# 会提供一个隐式的默认构造函数。如果我们提供任何类型的构造函数,则不会提供默认构造函数。

Program.cs
new Being();
new Being("Tom");

class Being
{
    public Being()
    {
        Console.WriteLine("Being is created");
    }

    public Being(string being)
    {
        Console.WriteLine($"Being {being} is created");
    }
}

我们有一个 Being 类。此类有两个构造函数。第一个不带参数;第二个带一个参数。

public Being(string being)
{
    Console.WriteLine($"Being {being} is created");
}

此构造函数接受一个字符串参数。

new Being();

创建了 Being 类的一个实例。这次在对象创建时调用不带参数的构造函数。

$ dotnet run
Being is created
Being Tom is created

在下一个例子中,我们初始化该类的数据成员。初始化变量是构造函数的典型工作。

Program.cs
var name = "Lenka";
var born = new DateTime(1990, 3, 5);

var friend = new MyFriend(name, born);
friend.Info();

class MyFriend
{
    private DateTime born;
    private string name;

    public MyFriend(string name, DateTime born)
    {
        this.name = name;
        this.born = born;
    }

    public void Info()
    {
        Console.WriteLine("{0} was born on {1}",
            this.name, this.born.ToShortDateString());
    }
}

我们有一个具有数据成员和方法的 MyFriend 类。

private DateTime born;
private string name;

我们在类定义中有两个私有变量。

public MyFriend(string name, DateTime born)
{
    this.name = name;
    this.born = born;
}

在构造函数中,我们初始化两个数据成员。this 变量是一个用于引用对象变量的处理程序。

var friend = new MyFriend(name, born);
friend.Info();

我们创建一个带有两个参数的 MyFriend 对象。然后我们调用对象的 Info 方法。

$ dotnet run
Lenka was born on 3/5/1990

C# 构造函数链接

构造函数链接是一个类从构造函数调用另一个构造函数的能力。要从同一个类调用另一个构造函数,我们使用 this 关键字。

Program.cs
new Circle(5);
new Circle();

class Circle
{
    public Circle(int radius)
    {
        Console.WriteLine($"Circle, r={radius} is created");
    }

    public Circle() : this(1) { }
}

我们有一个 Circle 类。该类有两个构造函数。一个带一个参数,另一个不带任何参数。

public Circle(int radius)
{
    Console.WriteLine("Circle, r={0} is created", radius);
}

此构造函数接受一个参数 — radius

public Circle() : this(1) { }

这是没有参数的构造函数。它只是调用另一个构造函数,并为其提供默认半径 1。

$ dotnet run
Circle, r=5 is created
Circle, r=1 is created

C# ToString 方法

每个对象都有一个 ToString 方法。它返回对象的人类可读表示形式。默认实现返回 Object 类型的完全限定名称。请注意,当我们使用对象作为参数调用 Console.WriteLine 方法时,将调用 ToString

Program.cs
var b = new Being();
var o = new Object();

Console.WriteLine(o.ToString());
Console.WriteLine(b.ToString());
Console.WriteLine(b);

class Being
{
    public override string ToString()
    {
        return "This is Being class";
    }
}

我们有一个 Being 类,我们在其中重写 ToString 方法的默认实现。

public override string ToString()
{
    return "This is Being class";
}

每个创建的类都继承自基类 objectToString 方法属于此类对象。我们使用 override 关键字来通知我们正在重写一个方法。

var b = new Being();
var o = new Object();

我们创建一个自定义定义的对象和一个内置对象。

Console.WriteLine(o.ToString());
Console.WriteLine(b.ToString());

我们在两个对象上调用 ToString 方法。

Console.WriteLine(b);

正如我们之前指定的,将对象作为参数放置到 Console.WriteLine 将调用其 ToString 方法。这次,我们隐式地调用了该方法。

$ dotnet run
System.Object
This is Being class
This is Being class

C# 对象初始化器

对象初始化器允许我们在创建时将值分配给对象的任何可访问字段或属性,而无需调用构造函数。属性或字段在 {} 括号内分配。此外,我们可以为构造函数指定参数或省略参数。

Program.cs
var u = new User { Name = "John Doe", Occupation = "gardener" };
Console.WriteLine(u);

class User
{
    public User() {}

    public string Name { set; get; }
    public string Occupation { set; get; }

    public override string ToString()
    {
        return $"{Name} is a {Occupation}";
    }
}

在该示例中,我们使用对象初始化器语法创建一个新用户。

public User() {}

我们定义一个空构造函数。

public string Name { set; get; }
public string Occupation { set; get; }

我们有两个属性:NameOccupation

var u = new User { Name = "John Doe", Occupation = "gardener" };

我们在 {} 括号内将值分配给属性。

$ dotnet run
John Doe is a gardener

C# 表达式体构造函数

可以创建表达式体构造函数;它们提供了更简洁、更好看的语法。

Program.cs
var u1 = new User("John Doe", "gardener");
var u2 = new User("Roger Roe", "driver");

Console.WriteLine(u1);
Console.WriteLine(u2);

class User
{
    private string Name;
    private string Occupation;

    public User(string Name, string Occupation) =>
        (this.Name, this.Occupation) = (Name, Occupation);

    public override string ToString() =>
        $"User {{ {this.Name} {this.Occupation} }}";
}

我们有一个带有两个参数的构造函数;它们在表达式体中设置。

public User(string Name, string Occupation) =>
    (this.Name, this.Occupation) = (Name, Occupation);

在这种情况下,this 关键字是必需的。

C# 目标类型的新表达式

当类型已知时,目标类型的新表达式不需要构造函数的类型规范。此功能是在 C# 9.0 中引入的。

Program.cs
var u1 = new User("Roger", "Roe", "driver");
Console.WriteLine(u1);

User u2 = new("John", "Doe", "gardener");
Console.WriteLine(u2);

var users = new List<User>
{
    new("Thomas", "Roove", "programmer"),
    new("Lucia", "Smith", "hair dresser"),
    new("Peter", "Holcomb", "painter"),
    new("Orlando", "Black", "actor"),
    new("Patrick", "Allen", "police officer")
};

foreach (var user in users)
{
    Console.WriteLine(user);
}


class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Occupation { get; set; }

    public User(string FirstName, string LastName, string Occupation) =>
        (this.FirstName, this.LastName, this.Occupation) = (FirstName, LastName, Occupation);

    public override string ToString() =>
        $"User {{ {this.FirstName} {this.LastName} {this.Occupation} }}";
}

我们演示用户类型的目标类型的新表达式。

var u1 = new User("Roger", "Roe", "driver");

可以使用 var 关键字来省略赋值左侧的类型声明,因为编译器可以从右侧推断类型。

User u2 = new("John", "Doe", "gardener");

目标类型的新表达式允许我们省略赋值右侧的类型声明。

var users = new List<User>
{
    new("Thomas", "Roove", "programmer"),
    new("Lucia", "Smith", "hair dresser"),
    new("Peter", "Holcomb", "painter"),
    new("Orlando", "Black", "actor"),
    new("Patrick", "Allen", "police officer")
};

在列表初始化程序中,我们通过省略每个用户的类型来节省一些击键。

C# 类常量

C# 允许创建类常量。这些常量不属于具体对象。它们属于该类。按照惯例,常量用大写字母书写。

Program.cs
Console.WriteLine(Math.PI);

class Math
{
    public const double PI = 3.14159265359;
}

我们有一个带有 PI 常量的 Math 类。

public const double PI = 3.14159265359;

const 关键字用于定义常量。public 关键字使其可以在类的主体外部访问。

$ dotnet run
3.14159265359

C# 继承

继承是一种使用已经定义的类来形成新类的方法。新形成的类称为派生类,我们从中派生的类称为类。继承的重要好处是代码重用和降低程序的复杂性。派生类(后代)覆盖或扩展基类(祖先)的功能。

Program.cs
new Human();

class Being
{
    public Being()
    {
        Console.WriteLine("Being is created");
    }
}

class Human : Being
{
    public Human()
    {
        Console.WriteLine("Human is created");
    }
}

在此程序中,我们有两个类。一个基类 Being 和一个派生类 Human。派生类继承自基类。

new Human();

我们实例化派生类 Human

class Human : Being

在 C# 中,我们使用冒号 (:) 运算符来创建继承关系。

$ dotnet run
Being is created
Human is created

我们可以看到两个构造函数都被调用了。首先,调用基类的构造函数,然后调用派生类的构造函数。

接下来是一个更复杂的示例。

Program.cs
new Human();

var dog = new Dog();
dog.GetCount();

class Being
{
    static int count = 0;

    public Being()
    {
        count++;
        Console.WriteLine("Being is created");
    }

    public void GetCount()
    {
        Console.WriteLine("There are {0} Beings", count);
    }
}

class Human : Being
{
    public Human()
    {
        Console.WriteLine("Human is created");
    }
}

class Animal : Being
{
    public Animal()
    {
        Console.WriteLine("Animal is created");
    }
}

class Dog : Animal
{
    public Dog()
    {
        Console.WriteLine("Dog is created");
    }
}

我们有四个类。继承层次结构更加复杂。HumanAnimal 类继承自 Being 类。Dog 类直接继承自 Animal 类,间接继承自 Being 类。我们还引入了 static 变量的概念。

new Human();

var dog = new Dog();
dog.GetCount();

我们从 Human 类和 Dog 类创建实例。我们调用 Dog 对象的 GetCount 方法。

static int count = 0;

我们定义一个 static 变量。静态成员是由类的所有实例共享的成员。

Being()
{
    count++;
    Console.WriteLine("Being is created");
}

每次实例化 Being 类时,我们将 count 变量增加 1。这样我们就可以跟踪创建的实例数量。

class Animal : Being
...

class Dog : Animal
...

Animal 继承自 BeingDog 继承自 Animal。间接地,Dog 也继承自 Being

$ dotnet run
Being is created
Human is created
Being is created
Animal is created
Dog is created
There are 2 Beings

Human 调用两个构造函数。Dog 调用三个构造函数。实例化了两个 Beings。

我们使用 base 关键字来显式调用父类的构造函数。

Program.cs
var c = new Circle(2, 5, 6);
Console.WriteLine(c);

class Shape
{
    protected int x;
    protected int y;

    public Shape()
    {
        Console.WriteLine("Shape is created");
    }

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

class Circle : Shape
{
    private int r;

    public Circle(int r, int x, int y) : base(x, y)
    {
        this.r = r;
    }

    public override string ToString()
    {
        return String.Format("Circle, r:{0}, x:{1}, y:{2}", r, x, y);
    }
}

我们有两个类:Shape 类和 Circle 类。Shape 类是几何形状的基类。我们可以将常见形状的一些共性放入此类中,例如 xy 坐标。

public Shape()
{
    Console.WriteLine("Shape is created");
}

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

Shape 类有两个构造函数。第一个是默认构造函数。第二个采用两个参数:x、y 坐标。

public Circle(int r, int x, int y) : base(x, y)
{
    this.r = r;
}

这是 Circle 类的构造函数。此构造函数初始化 r 成员并调用父类的第二个构造函数,它将 xy 坐标传递给该构造函数。如果我们没有使用 base 关键字显式调用该构造函数,则将调用 Shape 类的默认构造函数。

$ dotnet run
Circle, r:2, x:5, y:6

C# 多态

多态是以不同的方式对不同的数据输入使用运算符或函数的过程。在实践中,多态意味着如果类 B 继承自类 A,它不必继承类 A 的所有内容;它可以以不同的方式执行类 A 所做的一些事情。

一般来说,多态是以不同形式出现的能力。从技术上讲,它是为派生类重新定义方法的能力。多态涉及将特定实现应用于接口或更通用的基类。

多态是为派生类重新定义方法的能力。

Program.cs
namespace Polymorphism;

abstract class Shape
{
    protected int x;
    protected int y;

    public abstract int Area();
}

class Rectangle : Shape
{
    public Rectangle(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    public override int Area()
    {
        return this.x * this.y;
    }
}

class Square : Shape
{
    public Square(int x)
    {
        this.x = x;
    }

    public override int Area()
    {
        return this.x * this.x;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) };

        foreach (Shape shape in shapes)
        {
            Console.WriteLine(shape.Area());
        }
    }
}

在上面的程序中,我们有一个抽象 Shape 类。此类变形为两个后代类:RectangleSquare。两者都提供了 Area 方法的自己的实现。多态为 OOP 系统带来了灵活性和可扩展性。

public override int Area()
{
    return this.x * this.y;
}
...
public override int Area()
{
    return this.x * this.x;
}

RectangleSquare 类都有 Area 方法的自己的实现。

Shape[] shapes = { new Square(5), new Rectangle(9, 4), new Square(12) };

我们创建一个包含三个形状的数组。

foreach (Shape shape in shapes)
{
    Console.WriteLine(shape.Area());
}

我们遍历每个形状并调用其 Area 方法。编译器为每个形状调用正确的方法。这就是多态的本质。

C# 分部类

使用 partial 关键字,可以将类的定义拆分为同一命名空间内的几个部分。该类也可以在多个文件中定义。

当使用可以拆分为较小单元的非常大的代码库时,将使用分部类。分部类也用于自动代码生成器。

Program.cs
namespace PartialClass;

partial class Worker
{
    public string DoWork()
    {
        return "Doing work";
    }
}

partial class Worker
{
    public string DoPause()
    {
        return "Pausing";
    }
}

class Program
{
    static void Main(string[] args)
    {
        var worker = new Worker();

        Console.WriteLine(worker.DoWork());
        Console.WriteLine(worker.DoWork());
        Console.WriteLine(worker.DoPause());
    }
}

在该示例中,我们有两个部分定义的 Worker 类。这些部分由编译器连接在一起以形成最终类。

$ dotnet run
Doing work
Doing work
Pausing

来源

面向对象编程

在本文中,我们介绍了 C# 中的 OOP。

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。我自 2007 年以来一直在撰写编程文章。到目前为止,我已经撰写了 1,400 多篇文章和 8 本电子书。我在编程教学方面拥有超过十年的经验。

列出所有 C# 教程