2019 年 40 多个面向对象编程 (OOP) 面试题
到目前为止,OOP 是 IT 行业最常用的编程范式。所有主流编程语言现在都支持 OOP,包括 C#。OOP 反映了现实世界中事物的运作方式,也是建模和组织大型应用程序的最有效方法。
Q1:什么是继承?
主题:OOP
难度:⭐
继承允许我们根据一个类来定义另一个类,这使得应用程序的创建和维护更加容易。这也提供了重用代码功能的机会,并加快了实现速度。
创建类时,程序员不必编写全新的数据成员和成员函数,而是指定新类继承现有类的成员。这个现有类称为基类,新类称为派生类。
继承的概念实现了“IS-A”关系。例如,哺乳动物“IS A”是一种动物;狗“IS A”是一种哺乳动物,因此狗“IS A”也是一种动物;以此类推。
🔗资料来源: tutorialspoint.com
Q2:什么是面向对象编程(OOP)?
主题:OOP
难度:⭐
OOP 是一种开发逻辑模块的技术,例如包含属性、方法、字段和事件的类。程序中会创建一个对象来表示一个类。因此,对象封装了与类相关的所有特性,例如数据和行为。OOP 允许开发人员开发模块化程序并将其组装成软件。对象用于访问不同软件模块的数据和行为,例如类、命名空间和可共享程序集。.NET Framework 仅支持 OOP 语言,例如 Visual Basic .NET、Visual C# 和 Visual C++。
🔗资料来源: indiabix.com
Q3:什么是封装?
主题:OOP
难度:⭐⭐
封装是指将一个或多个对象封装在物理或逻辑包中的过程。在面向对象编程方法中,封装可以阻止访问实现细节。
🔗资料来源: tutorialspoint.com
Q4:什么是多态?
主题:OOP
难度:⭐⭐
多态性这个词的意思是具有多种表现形式。在面向对象编程范式中,多态性通常表现为一个接口,多个函数。
🔗资料来源: tutorialspoint.com
Q5:过程式编程和面向对象编程有什么区别?
主题:OOP
难度:⭐⭐
过程式编程基于模块化方法,将大型程序分解为多个过程。每个过程都是一组逐个执行的指令。而面向对象编程则基于对象。对象由各种元素组成,例如方法和变量。
过程式编程不使用访问修饰符,这意味着可以在程序中的任何位置自由访问整个数据。在面向对象编程 (OOP) 中,可以使用访问修饰符( public、private、internal、protected和protected internal )来指定特定数据的范围。
🔗资料来源: indiabix.com
Q6:解释一下构造函数的概念?
主题:OOP
难度:⭐⭐
构造函数是类的一个特殊方法,在创建类实例时会自动调用。它以与类相同的名称创建,并在访问类时初始化所有类成员。构造函数的主要特性如下:
- 构造函数没有任何返回类型。
- 构造函数可以重载。
- 声明构造函数不是强制性的;它由 .NET Framework 自动调用。
🔗资料来源: indiabix.com
Q7:代码中为什么要使用virtual关键字?
主题:OOP
难度:⭐⭐
在定义类时使用该virtual
关键字来指定该类的方法和属性可以在派生类中被覆盖。
🔗资料来源: indiabix.com
Q8:类和结构有什么区别?
主题:OOP
难度:⭐⭐
班级:
- 类是一种引用类型。
- 在实例化一个类时,CLR 会在堆中为其实例分配内存。
- 类支持继承。
- 类的变量可以被赋值为空。
- 类可以包含构造函数/析构函数。
结构:
- 结构是一种值类型。
- 在结构上,内存是在堆栈上分配的。
- 结构不支持继承。
- 结构成员不能具有空值。
- 结构不需要构造函数/析构函数,并且成员可以自动初始化。
🔗资料来源: indiabix.com
Q9:类和对象是什么关系?
主题:OOP
难度:⭐⭐
类就像一个蓝图,定义了许多对象共有的属性、状态和行为。对象是类的一个实例。例如,你有一个名为Vehicle的类,而Car就是该类的对象。你可以为名为Vehicle的类创建任意数量的对象,例如Van、Truck和Auto。
new运算符用于创建类的对象。实例化类的对象时,系统会为类中存在的每个数据成员分配内存。
🔗资料来源: indiabix.com
Q10:什么时候应该使用结构而不是类?
主题:OOP
难度:⭐⭐⭐
除非类型具有以下所有特征,否则不要定义结构:
- 它在逻辑上表示单个值,类似于原始类型(整数、双精度等)。
- 它的实例大小小于 16 字节。
- 它是不可变的。
- 不需要频繁地装箱。
🔗来源: stackoverflow.com
Q11:什么是多态性,它有什么用处,如何使用?
主题:OOP
难度:⭐⭐⭐
多态性描述了面向对象编程中的一种模式,其中类具有不同的功能但共享一个公共接口。
多态的美妙之处在于,处理不同类的代码无需知道它正在使用哪个类,因为它们的使用方式都相同。现实世界中,多态的类比是按钮。每个人都知道如何使用按钮:只需按下按钮即可。然而,按钮的“功能”取决于它所连接的对象以及使用的上下文——但结果并不影响它的使用方式。如果你的老板让你按下一个按钮,那么你已经掌握了执行该任务所需的所有信息。
🔗来源: stackoverflow.com
Q12:数据封装是什么意思?
主题:OOP
难度:⭐⭐⭐
数据封装是将数据和代码绑定在单个单元(称为对象)中,并向用户隐藏类的所有实现细节的概念。它可以防止未经授权的数据访问,并限制用户仅使用必要的数据。
🔗资料来源: indiabix.com
Q13:什么是抽象类?抽象类有哪些显著的特征?
主题:OOP
难度:⭐⭐⭐
抽象类是无法实例化的类,始终用作基类。
抽象类具有以下特征:
- 您无法直接实例化抽象类。这意味着您无法创建抽象类的对象;它必须被继承。
- 抽象类中可以有抽象成员,也可以有非抽象成员。
- 必须在抽象类中声明至少一个抽象方法。
- 抽象类始终是公共的。
- 使用abstract关键字声明抽象类。
抽象类的基本目的是提供多个派生类可以共享的基类的公共定义。
🔗资料来源: indiabix.com
Q14:类和结构之间有什么相似之处?
主题:OOP
难度:⭐⭐⭐
以下是类和结构之间的一些相似之处:
- 访问说明符(例如public、private和protected)在结构和类中以相同的方式使用,以限制对其主体之外的数据和方法的访问。
- 类成员和结构体成员(包括嵌套类和结构体)的访问级别默认为私有。私有嵌套类型无法从其包含类型外部访问。
- 两者都可以具有构造函数、方法、属性、字段、常量、枚举、事件和事件处理程序。
- 结构和类都可以实现接口以在代码中使用多重继承。
- 结构和类都可以有带参数的构造函数。
- 结构和类都可以有委托和事件。
🔗资料来源: indiabix.com
Q15:如何防止 C# 中的类被覆盖?
主题:OOP
难度:⭐⭐⭐
您可以使用sealed
关键字阻止类在 C# 中覆盖。
🔗资料来源: indiabix.com
Q16:方法覆盖与方法重载有何不同?
主题:OOP
难度:⭐⭐⭐
- 覆盖涉及在不同的类中创建两个或多个具有相同名称和相同签名的方法(其中一个应该是父类,另一个应该是子类)。
- 重载是在同一个类中的不同位置使用具有相同名称和不同签名的方法的概念。
🔗资料来源: indiabix.com
Q17:可以指定接口内方法的可访问性修饰符吗?
主题:OOP
难度:⭐⭐⭐
默认情况下,接口内的所有方法均为public
。您无法为它们指定任何其他访问修饰符。
🔗资料来源: indiabix.com
Q18:一个类可以继承其基类的构造函数吗?
主题:OOP
难度:⭐⭐⭐
不可以,类不能继承其基类的构造函数。
🔗资料来源: indiabix.com
Q19:内聚和耦合有什么区别?
主题:OOP
难度:⭐⭐⭐⭐
-
内聚力是指类(或模块)能做什么。
- 低凝聚力意味着班级会做各种各样的事情 - 它很广泛,没有重点关注它应该做的事情。
- 高内聚意味着类专注于它应该做的事情,即仅与类的意图相关的方法。
-
至于耦合度,它指的是两个类/模块之间的关联性或依赖性。对于低耦合度的类,修改一个类中的某些重要内容不会影响另一个类。高耦合度会使代码的修改和维护变得困难;由于类之间联系紧密,因此修改代码可能需要整个系统重构。
好的软件设计具有高内聚性和低耦合性。
🔗来源: stackoverflow.com
Q20:使用 getter 和 setter(仅获取和设置)而不是简单地使用这些变量的公共字段有什么好处?
主题:OOP
难度:⭐⭐⭐⭐
实际上,有很多很好的理由来考虑使用访问器而不是直接公开类的字段 - 除了封装和使未来的更改更容易之外。
以下是我所知道的一些原因:
- 与获取或设置属性相关的行为的封装 - 这允许以后更轻松地添加附加功能(如验证)。
- 隐藏属性的内部表示,同时使用替代表示来公开属性。
- 隔离公共接口以避免发生变更 - 允许公共接口在实现发生变化时保持不变,而不会影响现有的消费者。
- 控制属性的生命周期和内存管理(处置)语义 - 在非托管内存环境(如 C++ 或 Objective-C)中尤为重要。
- 为运行时属性发生变化时提供调试拦截点 - 在某些语言中,如果没有这个功能,调试何时何地属性更改为特定值可能会非常困难。
- 改进了与旨在针对属性 getter/setter 进行操作的库的互操作性 - 想到了 Mocking、Serialization 和 WPF。
- 允许继承者通过覆盖 getter/setter 方法来改变属性的行为和公开的语义。
- 允许将 getter/setter 作为 lambda 表达式而不是值传递。
- Getter 和 Setter 可以允许不同的访问级别 - 例如,get 可能是公开的,但 set 可能是受保护的。
🔗来源: stackoverflow.com
Q21:接口和抽象类到底有什么区别?
主题:OOP
难度:⭐⭐⭐⭐
- 接口是一种契约:编写接口的人说:“嘿,我接受这个样子”,而使用接口的人说:“好的,我写的类就是这样的”。接口是一个空壳。只有方法的签名,这意味着方法没有具体方法体。接口什么也做不了,它只是一种模式。实现接口消耗的 CPU 非常少,因为它不是类,只是一堆名称,因此不需要进行任何昂贵的查找。在嵌入式设备等重要场合,接口非常有用。
- 抽象类与接口不同,它本身就是类。它们的使用成本更高,因为继承它们时需要进行查找。抽象类看起来很像接口,但它们还有更多功能:你可以为它们定义行为。它更像是一个人说:“这些类应该看起来像这样,它们有共同点,所以填空吧! ”
🔗来源: stackoverflow.com
Q22:OOP中的耦合是什么?
主题:OOP
难度:⭐⭐⭐⭐
OOP 模块相互依赖。耦合是指两个软件模块之间的依赖程度。如果一个模块发生了变更,并且每次需要修改依赖模块时都需要支持该变更,那么这两个模块就高度依赖彼此。松耦合始终是首选。控制反转和依赖注入是实现模块松耦合的一些技术。
🔗来源: dotnetpattern.com
Q23:OOP 中的内聚性是什么?
主题:OOP
难度:⭐⭐⭐⭐
在面向对象编程 (OOP) 中,我们以模块的方式开发代码。每个模块都承担特定的职责。内聚性体现了模块职责之间的强关联程度。内聚性越高,代码质量越高。内聚性的好处包括:
- 改进模块的维护
- 提高可重用性
🔗来源: dotnetpattern.com
Q24:解释一下析构函数的概念?
主题:OOP
难度:⭐⭐⭐⭐
析构函数是类的一个特殊方法,在对象最终被销毁时自动调用。析构函数的名称与类的名称相同,但后面会添加一个前缀波浪号 (~)。
析构函数用于释放动态分配的内存和资源。不过,你可以实现一个自定义方法,通过调用析构函数来控制对象的销毁。
析构函数的主要特点如下:
- 析构函数没有任何返回类型
- 与构造函数类似,析构函数也始终是公共的
- 析构函数不能被重载。
🔗资料来源: indiabix.com
Q25:区分抽象类和接口。
主题:OOP
难度:⭐⭐⭐⭐
抽象类:
- 一个类只能扩展一个抽象类
- 抽象类的成员可以是私有的,也可以是受保护的。
- 抽象类应该有子类
- 任何类都可以扩展抽象类。
- 抽象类中的方法可以是抽象的,也可以是具体的。
- 抽象类可以有一个构造函数。
- 扩展抽象类的类可能会或可能不会实现其任何方法。
- 抽象类可以实现方法。
界面
- 一个类可以实现多个接口
- 接口只能有公共成员。
- 接口必须由类实现
- 只有一个接口可以扩展另一个接口。
- 接口中的所有方法都应该是抽象的
- 接口没有构造函数。
- 接口的所有方法都需要由实现该接口的类来实现。
- 接口不能包含任何方法的主体。
🔗资料来源: indiabix.com
Q26:什么是静态构造函数?
主题:OOP
难度:⭐⭐⭐⭐
C# 引入了静态构造函数,用于初始化类的静态数据。CLR 在创建第一个实例之前调用静态构造函数。
静态构造函数具有以下特点:
- 不需要访问说明符来定义它。
- 您不能在静态构造函数中传递参数。
- 一个类只能有一个静态构造函数。
- 它只能访问类的静态成员。
- 它仅在程序执行开始时调用一次。
🔗资料来源: indiabix.com
Q27:解释不同类型的继承。
主题:OOP
难度:⭐⭐⭐⭐
OOP 中的继承有四种类型:
- 单继承——包含一个基类和一个派生类
- 层次继承- 包含一个基类和同一基类的多个派生类
- 多级继承- 包含从派生类派生的类
- 多重继承- 包含多个基类和一个派生类
所有 .NET 语言都支持单继承、分层继承和多继承。它们不支持多重继承,因为在这些语言中,一个派生类不能有多个基类。但是,您可以通过接口在 .NET 中实现多重继承。
🔗资料来源: indiabix.com
Q28:.NET 支持多重继承吗?
主题:OOP
难度:⭐⭐⭐⭐
.NET 不直接支持多重继承,因为在 .NET 中,一个类不能从多个类继承。.NET 通过接口支持多重继承。
🔗资料来源: indiabix.com
Q29:如果原始方法不是静态的,那么是否可以将被重写的方法声明为静态的?
主题:OOP
难度:⭐⭐⭐⭐
不可以。两个虚方法必须具有相同的签名。
🔗资料来源: indiabix.com
Q30:为什么 C# 不允许静态方法实现接口?
主题:OOP
难度:⭐⭐⭐⭐⭐
我(简化的)技术原因是静态方法不在虚函数表中,调用点是在编译时选择的。这和你不能使用 override 或 virtual static 成员的原因是一样的。想要了解更多细节,你需要一个计算机科学毕业生或编译器专家——而我两者都不是。
你把一个接口传递给别人,他们需要知道如何调用方法。接口只是一个虚方法表(vtable)。你的静态类没有这个。调用者不知道如何调用方法。(在读到这个答案之前,我以为 C# 只是在吹毛求疵。现在我意识到这是接口本身造成的技术限制)。其他人会居高临下地跟你说这是一个糟糕的设计。其实这并不是糟糕的设计,而是一个技术限制。
🔗来源: stackoverflow.com
Q31:混合和继承有什么区别?
主题:OOP
难度:⭐⭐⭐⭐⭐
混合类(mix-in)是一个基类,你可以从中继承它来提供额外的功能。名称“mix-in”表明它旨在与其他代码混合使用。因此,推断你不会单独实例化混合类。混合类通常与其他基类一起使用。因此,**混合类是继承的一个子集,或者说是继承的特例 88。
与单继承相比,使用混合继承的优点在于,您只需编写一次功能代码,然后在多个不同的类中使用该功能。缺点在于,您可能需要在使用该功能的地方之外寻找它,因此最好将其放在附近以减轻这个缺点。
🔗来源: stackoverflow.com
Q32:以 OOP 程序员能够理解的术语(无需任何函数式编程背景),什么是 monad?
主题:OOP
难度:⭐⭐⭐⭐⭐
monad 是遵循某些规则并提供某些操作的类型“放大器”。
首先,什么是“类型放大器”?我指的是某种系统,它允许你将一个类型转换为更特殊的类型。例如,在 C# 中考虑Nullable<T>
。这就是一个类型放大器。它允许你将一个类型(比如 )添加int
到该类型,并为其添加一项新功能,即,现在它可以为 null 了,而之前它不能为 null。
第二个例子是IEnumerable<T>
。它是一个类型的放大器。它允许你接受一个类型,比如string
,并为该类型添加一项新功能,也就是说,你现在可以用任意数量的单个字符串创建一个字符串序列。
🔗来源: stackoverflow.com
Q33:什么是 LSP(里氏替换原则)?它的一些用法示例是什么(好的和坏的)?
主题:OOP
难度:⭐⭐⭐⭐⭐
里氏替换原则(LSP,lsp)是面向对象编程中的一个概念,它指出:
使用指向基类的指针或引用的函数必须能够在不知情的情况下使用派生类的对象。换句话说,可替换性是面向对象编程中的一项原则,它指出,在计算机程序中,如果 S 是 T 的子类型,则 T 类型的对象可以用 S 类型的对象替换。
考虑一下不好的例子:
csharp
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
public class Ostrich extends Bird{}
鸭子可以飞,因为它是鸟。鸵鸟是鸟,但它不能飞。鸵鸟类是鸟类的子类型,但它不能使用 fly 方法,这意味着我们违反了 LSP 原则。
正确的例子是:
csharp
public class Bird{
}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
🔗来源: stackoverflow.com
Q34:您能详细说明一下多态性、覆盖和重载吗?
主题:OOP
难度:⭐⭐⭐⭐⭐
-
多态性是指类实例能够表现得像其继承树中另一个类的实例(通常是其祖先类之一)的能力。例如,在 .NET 中,所有类都继承自 Object。因此,您可以创建一个 Object 类型的变量,并将任何类的实例赋值给它。
-
重写是一种函数类型,它出现在从另一个类继承的类中。重写函数“替换”了从基类继承的函数,但即使其类的实例通过多态性伪装成其他类型,重写函数也会被调用。参考前面的示例,您可以定义自己的类并重写 toString() 函数。由于此函数继承自 Object,因此即使您将该类的实例复制到 Object 类型的变量中,它仍然可用。通常,如果您在类伪装成 Object 时调用 toString(),则实际触发的 toString 版本是 Object 本身定义的版本。但是,由于该函数是重写的,因此即使类实例的真实类型隐藏在多态性背后,也会使用您类中 toString() 的定义。
-
重载是指定义多个具有相同名称但具有不同参数的方法的行为。它与覆盖或多态无关。
🔗来源: stackoverflow.com
Q35:“针对接口编程”是什么意思?
主题:OOP
难度:⭐⭐⭐⭐⭐
面向接口编程与我们在 Java 或 .NET 中看到的抽象接口完全无关。它甚至不是一个面向对象编程 (OOP) 的概念。它意味着只与对象或系统的公共接口交互。不必担心甚至预测它内部是如何运作的。不必担心它是如何实现的。在面向对象代码中,这就是为什么我们有公共和私有的方法/属性。
对于数据库来说,这意味着使用视图和存储过程而不是直接访问表。
使用接口是让代码易于测试的关键因素,它还能消除类之间不必要的耦合。通过创建定义类操作的接口,可以让想要使用该功能的类无需直接依赖实现类即可使用。如果之后你决定更改并使用其他实现,则只需更改实例化该实现的部分代码即可。其余代码无需更改,因为它依赖于接口,而不是实现类。
这在创建单元测试时非常有用。在被测类中,您可以让它依赖于接口,并通过构造函数或属性设置器将接口的实例注入到类中(或允许其根据需要构建接口实例的工厂)。该类在其方法中使用提供的(或创建的)接口。编写测试时,您可以模拟或伪造接口,并提供一个使用单元测试中配置的数据进行响应的接口。您可以这样做,因为被测类只处理接口,而不是具体的实现。任何实现该接口的类,包括您的模拟或伪造类,都可以这样做。
🔗来源: stackoverflow.com
Q36:您能否简单解释一下 OOP 上下文中的方法与函数?
主题:OOP
难度:⭐⭐⭐⭐⭐
函数是一段通过名称调用的代码。它可以被传递数据进行操作(即参数),也可以选择性地返回数据(即返回值)。所有传递给函数的数据都是显式传递的。
方法是指通过与对象关联的名称调用的一段代码。方法在大多数方面与函数相同,但有两个关键区别:
- 方法被隐式地传递给调用它的对象。
- 方法能够对类中包含的数据进行操作(记住,对象是类的一个实例 - 类是定义,对象是该类的一个实例)
🔗来源: stackoverflow.com
问题37:为什么组合比继承更受欢迎?每种方法各有哪些优缺点?什么时候应该选择继承而不是组合?
主题:OOP
难度:⭐⭐⭐⭐⭐
把包含想象成一种“有”关系。例如,一辆车“有一个”引擎,一个人“有一个”名字,等等。把继承想象成一种“是”关系。例如,一辆车“是”一辆交通工具,一个人“是”一只哺乳动物,等等。
组合优于继承,因为它更具延展性/易于后期修改,但不要使用“始终组合”的方法。使用组合,可以通过依赖注入/Setter 轻松动态更改行为。继承更为严格,因为大多数语言不允许从多个类型派生。因此,一旦从 TypeA 派生,事情就基本完蛋了。
我对上述情况的检验是:
- TypeB 是否希望暴露 TypeA 的完整接口(所有公共方法都应包含在内),以便 TypeB 可以在需要 TypeA 的地方使用?表示继承。
例如,塞斯纳双翼飞机将展示飞机的完整界面,甚至更多。因此,它适合从飞机派生。
- TypeB 是否只需要 TypeA 暴露的部分行为?这表明需要组合。
例如,一只鸟可能只需要飞机的飞行行为。在这种情况下,将其提取为接口/类/两者,并将其作为这两个类的成员是有意义的。
🔗来源: stackoverflow.com
Q38:关联、聚合和组合有什么区别?
主题:OOP
难度:⭐⭐⭐⭐⭐
- 关联是一种所有对象都有自己的生命周期并且没有所有者的关系。
让我们以“老师”和“学生”为例。多个学生可以与单个老师关联,单个学生也可以与多个老师关联,但对象之间没有所有权,并且都有各自的生命周期。两者都可以独立创建和删除。
- 聚合是关联的一种特殊形式,其中所有对象都有自己的生命周期,但有所有权,子对象不能属于另一个父对象。
让我们以部门和老师为例。一个老师不能同时属于多个部门,但如果我们删除部门,老师对象不会被销毁。我们可以将其视为一种“has-a”关系。
- 组合又是聚合的一种特殊形式,我们可以称之为“死亡”关系。它是一种强聚合。子对象没有生命周期,如果父对象被删除,所有子对象也将被删除。
让我们再举一个房屋 (House) 和房间 (Rooms) 关系的例子。房屋可以包含多个房间 - 每个房间没有独立的生命周期,并且任何房间都不能属于两个不同的房屋。如果我们删除房屋,房间也会被自动删除。
让我们再举一个问题和选项之间关系的例子。单个问题可以有多个选项,而选项不能同时属于多个问题。如果我们删除问题,选项也会被自动删除。
🔗来源: stackoverflow.com
Q39:你能在命名空间中声明一个私有类吗?
主题:OOP
难度:⭐⭐⭐⭐⭐
默认情况下,命名空间中的类是internal 的。但是,你可以显式地将它们声明为public,而不能声明为private、protected或protected internal。嵌套类可以声明为private、protected或protected internal。
🔗资料来源: indiabix.com
Q40:你在使用 C# 编程语言开发的类中定义了析构函数,但该析构函数从未执行。为什么析构函数没有执行?
主题:OOP
难度:⭐⭐⭐⭐⭐
运行时环境会自动调用类的析构函数来释放对象变量和方法所占用的资源。然而,在 C# 中,程序员无法控制析构函数的调用时机,因为垃圾收集器只负责释放对象所使用的资源。垃圾收集器会自动从 .NET 的运行时环境中获取未引用对象的信息,然后调用Finalize()方法。
虽然强制垃圾收集器执行垃圾收集并检索所有无法访问的内存并不是最好的选择,但程序员可以使用垃圾收集器类的Collect()方法强制执行垃圾收集器。
🔗资料来源: indiabix.com
文章来源:https://dev.to/aershov24/40-oop-interview-questions-in-2019-3mj2感谢🙌的阅读,祝你面试顺利!
如果喜欢,请分享给你的开发者同事!
更多 FullStack 面试问答,请访问👉 www.fullstack.cafe