C# OOP 精通 — 从测验答案到生产级思维模型

发布: (2025年12月24日 GMT+8 23:47)
13 min read
原文: Dev.to

Source: Dev.to

C# OOP 精通 — 从测验答案到生产级思维模型

大多数 C# 面试不会让你构建完整的企业系统。
相反,它们会悄悄地测试你是否真正理解 OOP 基础背后的 思维模型

  • 定义类创建对象 有什么区别?
  • 为什么方法代表 行为
  • C# 能否从 多个类 继承?
  • 为什么要使用构造函数(以及何时更倾向于对象初始化器)?
  • 访问修饰符到底控制什么?
  • 为什么在现代 C# 中 record 类型是 “特殊”的?

在本文中,我们将把测验式的问题转化为 可在生产环境中复用的模式,帮助你在以下场景中使用:

  • 代码审查
  • 架构讨论
  • 技术面试
  • 实际 .NET 服务

TL;DR — 您将学习

  • 类型 vs. 实例 思维模型(class vs. new
  • 为什么方法是 行为,而不仅仅是“函数”
  • 实际规则:单类继承多接口
  • 构造函数、对象初始化器和 不变式
  • 使用属性进行封装:get; set;(以及高级开发者如何限制 setter)
  • 访问修饰符:不是“是否存在”,而是 谁可以访问它
  • protected 的真实情况(以及何时它成为设计异味)
  • record 存在的原因:值相等、不可变性和数据建模

1) “哪个关键字创建对象?” — 定义 vs. 实例化

测验版本

“在 C# 中,用什么关键字或指令来创建对象?”

答案: new

高级思维模型

  • class 定义一种类型(蓝图)
  • new 创建实例(托管堆上的运行时对象,由构造函数初始化)
public class Person
{
    public string Name { get; }
    public Person(string name) => Name = name;
}

var p = new Person("Carlos"); // ✅ 创建对象(实例)

面试金句

class 定义蓝图;new 构建真实实例并运行构造函数。”

2) “方法在类内部定义了什么?” — 行为

测验版本

“方法在类内部定义了什么?”

答案: 行为

为什么这在生产环境中很重要

  • Properties/fields 表示 state
  • Methods 表示 behavior——改变该状态的 rules
public sealed class BankAccount
{
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        if (amount > 0)
        {
            Balance += amount;
        }
        // else ignore or throw
    }
}

“Methods encode business rules. Properties hold data. Behavior protects invariants.”

3) “C# 只允许实现一个接口” — 错误

测验版本

“C# 只允许实现一个接口。”

答案: 错误

真正规则(必须记住)

  • ✅ 一个类可以实现 多个接口
  • ❌ 一个类只能继承 一个基类
public sealed class ExportService : IDisposable, IAsyncDisposable, IHostedService
{
    public void Dispose() { /*...*/ }
    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    public Task StartAsync(CancellationToken ct) => Task.CompletedTask;
    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}

面试金句

“类采用单继承,接口采用多继承。”

Source:

4) “我们如何在创建时初始化属性?” — 构造函数(及其相关)

测验版

“我们如何在对象创建时初始化其属性?”

答案: 使用构造函数

高级细节:构造函数 vs. 对象初始化器

两者都可以初始化值,但它们的目标不同。

✅ 构造函数 = 强制不变量

当对象必须以有效状态创建时使用。

public sealed class Order
{
    public Guid Id { get; }
    public string CustomerEmail { get; }

    public Order(Guid id, string customerEmail)
    {
        if (string.IsNullOrWhiteSpace(customerEmail))
            throw new ArgumentException("Email required.", nameof(customerEmail));

        Id = id;
        CustomerEmail = customerEmail;
    }
}

✅ 对象初始化器 = 可选字段的易用性

非常适合 DTO 或选项袋。

var opts = new HttpClientOptions
{
    Timeout = TimeSpan.FromSeconds(10),
    Retries = 3
};

面试金句

“构造函数用于建立不变量。对象初始化器则为可选配置提供便利。”

5) “一个类可以作为另一个类的属性类型” — 组合

答案: True

这是一种组合:一种 has‑a(拥有)关系(通常比继承更好)。

public sealed class Address
{
    public string Street { get; init; } = "";
}

public sealed class Person
{
    public Address Address { get; init; } = new();
}

为什么资深开发者更倾向于组合

  • 继承会导致紧耦合。
  • 组合保持边界更清晰,并且支持替换。

面试金句

“更倾向于使用组合:它比深层继承链更容易重构和测试。”

6) “访问修饰符定义 …” (原文内容被截断)

测验版本

“访问修饰符定义成员的可见性。”

答案: True

实际控制的内容

修饰符可访问范围
public任意位置
internal同一程序集
protected派生类型(任意程序集)
protected internal派生类型 同一程序集
private protected同一程序集中的派生类型
private仅限包含类型

高级视角

  • 封装 关注的是 谁可以触及 成员,而不是 成员是否存在
  • 过度公开成员(public)可能成为设计异味。
  • 应优先使用最严格的修饰符,只要能满足实际使用需求。
public abstract class Shape
{
    // Only derived types can read/write the radius.
    protected double Radius { get; set; }

    // Only this class can change the ID after construction.
    public Guid Id { get; private set; } = Guid.NewGuid();

    // Internal helper used by the assembly but hidden from consumers.
    internal void Validate() { /*...*/ }
}

面试金句

“访问修饰符是一种关于 可以与成员交互的契约,而不是对成员存在的声明。”

7) “为什么会有 record 类型?” — 值相等与不可变性

测验版

“在现代 C# 中,record 类型有什么特别之处?”

答案: 值相等、内置不可变性以及简洁的数据建模

关键好处

  • 自动生成基于 值的 Equals/GetHashCode
  • 使用 with 表达式 进行非破坏性修改。
  • DTO领域模型 提供简洁语法。
public record PersonDto(string FirstName, string LastName);
var p1 = new PersonDto("Alice", "Smith");
var p2 = p1 with { LastName = "Johnson" }; // 创建一个新的不可变实例
bool same = p1 == new PersonDto("Alice", "Smith"); // true – 值相等

面试金句

“当你需要不可变、值语义的数据载体时使用 record;对具有身份语义的实体则避免使用。”

结束语

理解这些 OOP 基础背后的 原因 能帮助你:

  • 编写能够 强制不变量保护状态 的代码。
  • 正确选择 继承 vs. 组合 的策略。
  • 代码评审面试 中清晰沟通。

掌握这些思维模型,你就能从回答测验题目转向构建健壮、生产级的 .NET 应用。

C# 面试快速检查

1️⃣ “一个类拥有属性和方法的类拥有” — 错误

正确答案: 错误

现实情况
访问修饰符定义的是 可见性,而不是成员是否存在。

public sealed class User
{
    public string Email { get; private set; } = "";
    private string PasswordHash { get; set; } = "";
}

面试金句
“访问修饰符回答的是‘谁可以触碰这个成员?’而不是‘它是否存在?’”

2️⃣ “C# 能从多个类继承吗?” — 不能

正确答案: 只能继承一个类

public class Employee : Person // ✅
{
}

我们在 C# 中如何模拟“多重继承”

  • 接口 用于能力抽象
  • 组合 用于行为复用
  • 装饰器委托 用于横切关注点

面试金句
“C# 避免了多类继承——接口 + 组合提供了灵活性,同时没有菱形继承的问题。”

3️⃣ “哪个符号给属性赋值?” — =

正确答案: =

person.Name = "Juan"; // 赋值运算符

高级开发者关注这一点,因为赋值往往是 不变量被破坏 的地方。
因此你经常会看到:

  • private set;
  • init;
  • 用于安全变更的成员方法

4️⃣ protected 访问范围 — 正确(但需谨慎使用)

正确答案: 正确

protected 的含义是:

  • 在声明它的类型内部可访问
  • 在派生类型内部可访问
public class Base
{
    protected int Value;
}

public class Derived : Base
{
    public void Set() => Value = 10; // ✅ 允许
}

高级警告
protected 可能导致“泄漏的继承”设计,派生类依赖于脆弱的内部细节。
使用场景:

  • 基类明确设计为可扩展
  • 你能够控制继承层次结构
  • 你拥有稳定且有文档说明的模板方法模型

5️⃣ 记录(record)因为值相等而重要 — 正确

正确答案: 正确

record 存在的原因

记录类型专为 数据建模 而设计:

  • 基于值的相等性
  • 简单的不可变性
  • 结构化比较

非常适合 DDD 中的消息 / 事件 / DTO。

public record Person(string Name, int Age);

var p1 = new Person("Ana", 30);
var p2 = new Person("Ana", 30);

Console.WriteLine(p1 == p2); // ✅ true (值相等)

面试金句
“记录用于具有值语义的数据——非常适合 DTO 和领域事件。”

6️⃣ 接口关键字 — interface

public interface ILogger
{
    void Log(string message);
}

7️⃣ 类关键字 — class

public class Order { }

8️⃣ “面向对象的基本概念” — 类和对象

正确答案: class 和 object(类和对象)

其他概念(继承、多态、抽象、封装)都是建立在这一基础之上的。

9️⃣ C# 中的多态 — 重写(以及接口)

正确答案: Overwrite(覆盖)

public abstract class Animal
{
    public abstract string Speak();
}

public sealed class Dog : Animal
{
    public override string Speak() => "Bark";
}

🔟 抽象声明 — 正确

正确答案: 正确

抽象定义 做什么,而不指定 怎么做

  • 接口
  • 抽象类

1️⃣1️⃣ “属性需要考虑哪些因素?” — 访问级别、类型、名称

正确答案: A. 访问级别、类型和名称

public string Name { get; private set; } = "";

1️⃣2️⃣ 封装关键字 — get; set;

正确答案: get; set;

高级升级
封装并不是“到处都用 public get/set”。它是对变更的控制。

public sealed class Customer
{
    public string Email { get; private set; }

    public Customer(string email)
    {
        // 验证并规范化
        Email = email.Trim().ToLowerInvariant();
    }

    public void ChangeEmail(string newEmail)
    {
        Email = newEmail.Trim().ToLowerInvariant();
    }
}

It looks like the text you’d like translated didn’t come through. Could you please resend the content (the part you want translated to Simplified Chinese)? Once I have the text, I’ll provide the translation while preserving the source link, formatting, and any code blocks as you requested.

最终思考 — 为什么这些“基本”答案很重要

  • SOLID(封装、抽象、可替换性)
  • Clean Architecture(边界、组合)
  • Dependency Injection(接口、对象图创建)
  • Maintainability(不变量和受控变更)

✍️ 作者 Cristian Sifuentes — 构建弹性 .NET 系统,教导团队如何思考面向对象编程、边界以及清晰架构。

Back to Blog

相关文章

阅读更多 »

SOLID 再探 — 后模式视角

为什么原则不如背后的力量重要:SOLID 不是一份检查清单,而是对更深层力量的历史压缩。这是系列的第 5 部分。