ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

类、对象与方法:从订单业务讲透面向对象设计与多语言实现

类、对象与方法:从订单业务讲透面向对象设计与多语言实现 做业务开发这些年我带过不少新人也在社区里回答过大量问题。有个现象特别有意思很多人能照着教程把代码敲出来跑通了就以为懂了可一旦需求稍微变个形比如给订单加个优惠券字段同一个商品在不同渠道价格不一样立刻就卡住了。卡住的根本原因往往不是语法不熟而是脑子里没有类、对象、方法这三样东西的清晰模型。类、对象、方法这三个词看着像教科书里的入门概念实际上它们决定了你的代码是能长成参天大树的骨架还是三个月后自己都不敢碰的一团乱麻。这篇文章我不打算按教材顺序念定义而是从你在写业务时到底在做什么这个角度切进去把类的设计、对象的创建与生命周期、方法的定义与调用讲透同时把 Python、Java、JavaScript、C# 几个常见语言的写法摆在一起对照让你换语言的时候不至于重新学一遍。内容适合刚学完语法、准备写第一个真实项目的朋友也适合写了几年代码但总觉得自己的类设计得不太对的同学。1. 先把概念落地类、对象、方法到底在说什么1.1 用一个外卖订单把三个词讲明白假设你要做一个外卖系统最核心的一件事就是处理订单。现在你手上有三条真实的订单数据订单号 A001金额 38 元状态待支付订单号 A002金额 52 元状态已支付订单号 A003金额 19 元状态已取消。如果你只会用变量和函数很可能写成order_id_1 A001、amount_1 38、status_1 待支付再来一套_2、_3。三条数据还能忍三千条呢这时候你会发现真正的问题不是数据太多而是一条订单这个整体被你拆散成了互不相干的碎片谁都能改改错了也看不出来。类要解决的就是这件事。类是把一条订单应该有哪些数据、能做哪些事写在一起的一份说明书。上面的例子里订单就是类它规定了每条订单都必须有订单号、金额、状态并且必须支持加菜品支付取消这几个动作。对象则是按照这份说明书真正造出来的那一条条具体订单A001 是一个对象A002 是另一个对象它们各自有各自的数据互不干扰。方法呢就是挂在对象身上、用来操作这些数据的函数比如支付这个方法它会去检查订单状态、修改状态、打印日志。这里有个特别容易混淆的点值得先说清楚很多人以为类是大一点的结构体只是把几个字段打包在一起。这个理解会让你写出一堆只有字段没有行为的贫血对象然后所有逻辑散落在外部函数里最终还是回到面条代码。类和普通数据结构的本质区别在于它同时封装了数据和操作数据的规则。金额不能为负这条规则写在类的构造函数或者加菜方法里任何人想绕过它都得先过这一关这才是类存在的意义。1.2 模板与实例对象创建时到底发生了什么对象创建这个词听起来很玄其实拆开看就是三步分配内存、初始化字段、返回引用。以 Java 为例当你写下new Order(A001, 38.0)的时候运行时会先在堆上找一块足够放下这个对象的内存把所有字段先置为默认值数字是 0布尔是 false引用是 null然后调用构造方法把orderId设置成 A001amount设置成 38.0。注意这个顺序很重要字段先被默认值填充再执行构造方法体。这就解释了为什么在 Java 里构造方法中如果调用了被子类重写的方法可能读到还没赋值的字段这类坑在大型项目里非常隐蔽。Python 的流程略有不同。Order(A001, 38.0)会先调用__new__造出一个空对象再调用__init__往里面塞属性。Python 不要求你在类里预先声明字段self.amount 38这一行执行完属性才真正存在。这种灵活性是把双刃剑写起来快但字段散落在各个方法里IDE 补全不出来拼错名字也不会报错只有运行到那一行才炸。我在实际项目里养成的习惯是Python 类一定在__init__里把所有属性初始化一遍哪怕是self.coupon None这种看起来多余的写法它换来的是这个对象有哪些字段一目了然。JavaScript 在 ES6 之后有了class关键字但要注意它本质上是基于原型的语法糖。new Order()做的事情是创建一个新对象把它的原型指向Order.prototype然后执行构造函数。方法之所以被所有实例共享是因为它们挂在原型上而不是每个实例复制一份。用Object.getPrototypeOf(obj)就能看到这条链。C# 则和 Java 比较接近值类型和引用类型在创建时的行为差异很大结构体是复制语义类是引用语义选错了会导致我明明改了对象外面却没变这种经典困惑。1.3 方法为什么要挂在类上而不是写成普通函数有朋友问过我既然方法就是个函数为什么不干脆写成pay(order, channel)这样的独立函数答案是工程上的可维护性。当系统里有几百个函数的时候pay这个名字可能会被用在支付订单、支付工资、支付供应商货款上命名冲突的处理会越来越别扭。挂到类上以后order.pay(channel)和salary.pay(channel)天然区分开了IDE 里敲order.就能列出所有相关操作读代码的人不用跳来跳去查文档。方法从功能上大致分四类这个分类很多人没系统想过。第一类是访问器比如get_amount()只读不写第二类是修改器比如add_item()会改变对象状态第三类是业务动作比如pay()、cancel()通常会做状态检查并产生副作用第四类是工具方法比如校验订单号格式它和具体某条订单没关系更适合写成静态方法。把这四类分清楚你的类接口会干净很多。最常见的设计失误是把工具方法写成实例方法结果为了校验一个字符串不得不先造一个对象出来白白浪费一次创建。还有一个细节是方法的返回值设计。add_item返回什么返回None也行但返回self就能支持链式调用order.add_item(米饭, 3).add_item(可乐, 5).pay(微信)读起来顺畅很多。不过要小心链式调用会掩盖状态变化调试的时候不容易看出是哪一步出了问题我在团队里一般只在构建类场景比如查询条件拼接用链式涉及业务状态流转的地方还是老老实实分开写。2. 类的设计从需求到字段和方法的映射2.1 怎么从一段业务描述里抽出类拿一段典型的甲方需求举例用户可以下单一个订单可以包含多个商品每个商品有名称和单价订单可以应用优惠券优惠券有满减和折扣两种支付成功后订单状态变为已支付。 新手看到这段话第一反应往往是开始写流程代码先查用户、再循环商品、再算优惠、再改状态。这样写出来的是一条线性的脚本能跑但改起来要命。有经验的人会先做名词提取。这段话里的名词有用户、订单、商品、优惠券。这些就是候选的类。然后看动词下单、包含、应用、支付。这些是候选的方法再判断方法该挂到谁身上下单是用户的行为包含是订单和商品的关系应用是订单对优惠券的操作支付是订单自身状态的变化。这样一拆用户有方法create_order()订单有方法add_item()、apply_coupon()、pay()优惠券有方法calculate_discount(amount)。这里有个判断依据值得记住方法应该挂到它主要操作的那个数据所属的类上。算优惠是针对订单金额算的优惠券提供打折规则所以更合理的做法是订单调用coupon.calculate_discount(self.amount)让优惠券专注自己的规则订单专注自己的金额。这样将来加一种限时折扣券只需要新增一个类实现同样的方法名订单代码一行都不用改。这就是面向对象里那个被说烂了但确实有用的概念——多态。2.2 字段、属性与构造函数的设计取舍字段设计最核心的原则是不变式。什么叫不变式就是任何时候都必须成立的事实。比如订单的金额不能为负状态只能取几个固定值之一订单号创建后不能改。把这些约束想清楚字段的可见性就定了订单号应该是finalJava或者只提供读方法Python 用property状态只能通过明确的方法变更金额的变化必须走add_item或apply_coupon这类经过校验的入口。构造函数的设计有个很实用的技巧只接受让对象处于合法状态所必需的数据。订单必须要有订单号和初始金额吗其实订单号可以自动生成初始金额可以是 0所以真正必需的可能只有用户 ID。但为了测试方便我一般会留一个可以指定订单号的重载。Java 里用this(orderId, 0.0)实现构造方法链Python 里用默认参数加classmethod工厂方法。说到默认参数Python 有个著名的坑必须提。看这段代码class Cart: def __init__(self, items[]): # 危险写法 self.items items def add(self, name): self.items.append(name)你以为每个购物车都从空列表开始实际上所有实例共享同一个列表对象。原因在于默认参数在函数定义时只求值一次[]这个列表被创建了一次之后每次调用都用同一个。正确写法是def __init__(self, itemsNone): self.items items if items is not None else []。这个坑我见过太多次了表现出来往往是我新建的购物车里居然有别人加的东西排查起来要花很久。属性property是另一个值得掌握的机制。Python 的property能把方法伪装成属性访问好处是可以在读取时做计算或校验而且将来把普通属性改成 property调用方代码不用改。Java 里对应的是 getter/setter虽然啰嗦但主流框架比如各种序列化库都依赖这套约定。C# 的属性语法最舒服public double Amount { get; private set; }一行同时表达了外部可读、内部可写。2.3 封装、继承、多态该用和不该用的边界封装这个概念经常被简化成把字段设为 private这理解太窄了。封装的真正含义是对象对外只暴露稳定的接口内部实现随便你怎么改。举个例子订单金额原本用 double 存后来发现浮点误差导致对账差几分钱改成用整数分存储只改get_amount()的返回逻辑就行所有调用方无感知。如果当初大家都直接访问order.amount字段这次重构就要动几十个文件。所以封装的收益不是安全性而是可变更性。继承是最容易被滥用的机制。判断标准很简单只有满足is-a关系才用继承而且子类必须能完全替换父类。订单和优惠券是有一个的关系所以订单应该持有一个优惠券对象组合而不是继承优惠券。我在代码评审里见过class DiscountedOrder extends Order这种写法一开始挺自然后来加了订单既打折又用券的需求立刻变成多继承的困境。组合则是加一个ListCoupon字段就能解决。多态的价值在扩展点。假设优惠券有三种算法不用多态就得写if type 满减: ... elif type 折扣: ...每加一种就改一次这个 if风险越来越大。用多态的话订单只调coupon.calculate_discount(amount)新增类型时只写一个新类订单代码零改动。这就是常说的对扩展开放对修改关闭。不过也别走极端只有两三种且几乎不会变的场景用 if 反而更直白。抽象类和普通类的区别也在这里抽象类适合有一份公共逻辑要复用同时留几个方法给子类实现如果只想要一份接口契约完全可以用接口Java 的 interface、Python 的抽象基类来避免继承带来的耦合。3. 动手实现从零写一个能跑的订单类3.1 Python 版最小可用类与实例化先给一个可以直接复制运行的完整版本覆盖了类属性、实例属性、实例方法、类方法、静态方法这几种常见形态class Order: # 类属性所有实例共享适合放常量 PLATFORM 自营 _VALID_STATUS {created, paid, cancelled} def __init__(self, order_id, amount0.0, itemsNone): self.order_id order_id self.amount float(amount) self.items list(items) if items else [] self._status created def add_item(self, name, price): if price 0: raise ValueError(菜品价格必须大于 0) self.items.append({name: name, price: price}) self.amount price return self def pay(self, channel): if self._status ! created: raise RuntimeError(f当前状态 {self._status} 不允许支付) self._status paid print(f[{self.PLATFORM}] {self.order_id} 通过 {channel} 支付 {self.amount:.2f} 元) return True property def status(self): return self._status classmethod def from_cart(cls, cart): 工厂方法从购物车快照创建订单 return cls(cart[id], cart[total], cart.get(items)) staticmethod def is_valid_id(order_id): return isinstance(order_id, str) and len(order_id) 12 def __repr__(self): return fOrder {self.order_id} {self._status} {self.amount:.2f}调用起来是这样的order Order(A00120240001) order.add_item(宫保鸡丁, 32).add_item(米饭, 3) order.pay(微信) print(order.status, order.amount)这里有几个设计点值得说明。_status用下划线开头是 Python 社区的约定表示内部字段别直接改虽然语言层面拦不住但配合property提供了只读访问实际使用中足够有效。_VALID_STATUS定义了合法状态集合将来加状态时只改这一处。from_cart用cls而不是硬写Order这样将来有RefundOrder(Order)子类调用时创建出来的是子类实例这是类方法相比静态方法的关键优势。静态方法is_valid_id不需要self也不需要cls它就是个放在类命名空间里的普通函数。什么时候该用静态方法当这个逻辑和类强相关、但不需要访问任何实例或类状态时。放在类里能让调用方通过Order.is_valid_id(...)找到它比散落在模块里更好发现。3.2 Java 版构造链、this 与访问控制同样的订单用 Java 写能更清楚地看到类型约束和访问控制import java.util.ArrayList; import java.util.List; public class Order { private static final String PLATFORM 自营; private static final ListString VALID_STATUS List.of(created, paid, cancelled); private final String orderId; private double amount; private final ListString items new ArrayList(); private String status created; public Order(String orderId) { this(orderId, 0.0); // 构造方法链 } public Order(String orderId, double amount) { if (orderId null || orderId.isBlank()) { throw new IllegalArgumentException(订单号不能为空); } if (amount 0) { throw new IllegalArgumentException(金额不能为负); } this.orderId orderId; this.amount amount; } public Order addItem(String name, double price) { if (price 0) { throw new IllegalArgumentException(菜品价格必须大于 0); } items.add(name); amount price; return this; // 支持链式调用 } public void pay(String channel) { if (!created.equals(status)) { throw new IllegalStateException(当前状态 status 不允许支付); } status paid; System.out.printf([%s] %s 通过 %s 支付 %.2f 元%n, PLATFORM, orderId, channel, amount); } public String getOrderId() { return orderId; } public double getAmount() { return amount; } public String getStatus() { return status; } Override public String toString() { return Order{ orderId , status , amount }; } Override public boolean equals(Object o) { if (this o) return true; if (!(o instanceof Order)) return false; Order other (Order) o; return orderId.equals(other.orderId); } Override public int hashCode() { return orderId.hashCode(); } }几点解释。this(orderId, 0.0)必须在构造方法第一行这是 Java 的硬性规定。final字段一旦赋值就不能改正好对应订单号创建后不可变这条业务规则。addItem返回this让最后的toString里字段拼接看着简单同时支持链式调用。关于构造方法修饰符要不要跟类一样这是个常被问到的问题。答案是不需要也不能乱写。构造方法可以被public、protected、private修饰public谁都能 newprivate只有类内部能 new常用于单例模式protected只有同包和子类能 new。它不能被static、final、abstract修饰因为这三种修饰符对构造方法没有意义——构造方法本来就是在实例化时调用的static 无从谈起。如果你写了static Order(...)编译器会直接报错。3.3 JavaScript 与 C# 的写法对照JavaScript 用私有字段和静态字段的方式是这样class Order { static PLATFORM 自营; #status created; // 真私有外部访问会直接报错 constructor(orderId, amount 0) { if (!orderId) throw new Error(订单号不能为空); this.orderId orderId; this.amount amount; this.items []; } addItem(name, price) { if (price 0) throw new Error(价格必须大于 0); this.items.push({ name, price }); this.amount price; return this; } pay(channel) { if (this.#status ! created) { throw new Error(当前状态 ${this.#status} 不允许支付); } this.#status paid; return true; } get status() { return this.#status; } }#status是 ES2022 引入的真私有字段和 Python 下划线约定不同它由语言层面强制保护。这一点在做前端状态管理时特别有用能避免组件外部直接改状态导致视图不同步。C# 的属性语法最简洁public class Order { public static string Platform { get; } 自营; public string OrderId { get; } public double Amount { get; private set; } public string Status { get; private set; } created; public Order(string orderId, double amount 0) { if (string.IsNullOrWhiteSpace(orderId)) throw new ArgumentException(订单号不能为空); OrderId orderId; Amount amount; } public Order AddItem(double price) { if (price 0) throw new ArgumentOutOfRangeException(nameof(price)); Amount price; return this; } public void Pay(string channel) { if (Status ! created) throw new InvalidOperationException($当前状态 {Status} 不允许支付); Status paid; } }{ get; private set; }表达的语义是外部只能读类内部可以改一行顶 Java 的四行。不过要注意 C# 里如果类要作为字典键得重写Equals和GetHashCode默认是按引用比较的这一点和 Java 一致。3.4 方法参数的传递方式与可变参数参数传递是另一个高频误区。Java 和 C# 都是值传递但对象类型的值是引用所以传对象进去改它的属性外面能看到变化而重新给参数赋值order new Order(...)不会影响外面的变量。Python 也是类似语义叫传对象引用。很多教程说Java 对象是引用传递严格讲不准确容易让人以为能在方法里替换调用方的对象。可变参数Java 的String...、Python 的*args、C# 的params适合参数个数不确定的场景比如记录日志、拼接标签。但有个坑可变参数底层是数组如果调用方传的是 null很容易在方法内部 NPE。所以方法内第一步通常要做空值判断。另外Python 的*args和**kwargs虽然灵活但会破坏 IDE 的参数提示和类型检查在公开接口上我一般会显式列出参数只在装饰器、包装器这类场景用可变参数。关于数组方法和数组对象去重这在前后端都很常见。JavaScript 里给对象数组去重最稳的做法是按业务主键建 Mapconst map new Map(list.map(o [o.id, o])); const unique [...map.values()];。为什么不用[...new Set(list)]因为 Set 用的是引用相等两个内容相同但引用不同的对象不会被去重。这个坑我在处理接口重复数据时踩过当时数据看起来一模一样就是去不掉排查半天才发现是引用问题。4. 常见问题与排查技巧实录4.1 高频报错速查表下面这张表是我这些年遇到最多的问题按语言分了类排查时可以直接对号入座报错或现象常见原因排查思路找不到或无法加载主类classpath 配置错误、包名与目录不一致、编译产物没生成先确认.class文件是否生成再用-cp显式指定路径检查 package 声明表达式必须包含类类型把变量名当成类型用了或者缺少new检查那一行是不是漏了new或者变量名和类名冲突Python 导入上级目录的类失败sys.path 不含父目录或缺少__init__.py用sys.path.append临时验证长期方案是规范包结构和安装方式对象属性改了外面没变传的是值类型或者方法内重新赋值了参数确认类型是引用类型确认方法内是修改字段而非重新赋值判断两个对象不相等默认按引用比较没重写 equalsJava 用equalsPython 重写__eq__JS 比较唯一键静态字段被意外共享误把该做实例字段的写成了 static检查字段是否包含状态有状态的字段不能 static空指针异常字段未初始化、返回值可能为 null构造方法里把所有字段初始化返回值用 Optional 或判空表达式必须包含类类型这条报错特别值得说一句。它通常出现在 Java 里你写Order order Order();的时候编译器以为Order()是个方法调用但Order是类不是方法于是提示你这里需要一个类型。加上new就好了。类似的错误在 C 里表现为缺少类型说明符本质都是同一个问题把类名当成函数用了。C 里还有个前置声明的场景如果你只写了class Order;而没有包含完整定义就只能用Order*指针不能创建对象或调用方法编译期会提示类型不完整这时候要么补上头文件要么把实现挪到 cpp 文件里。4.2 对象比较、拷贝与去重的几个坑对象比较是另一个重灾区。Java 的比较的是引用地址两个内容完全相同的订单用判断一定是 false必须用equals。但重写equals就一定要重写hashCode否则放进 HashSet 会出现明明有重复却都放进去了的情况。原因是哈希集合先按 hashCode 找桶再用 equals 比较hashCode 不一致的话根本走不到比较那一步。IDE 一般能自动生成这两个方法但生成时要确认用哪些字段参与比较——通常用业务主键订单号就够用全部字段容易因为一个无关字段变化导致判断失准。Python 里对应的机制是__eq__和__hash__。默认情况下对象按身份比较a b等价于a is b。一旦你重写了__eq__Python 会自动把__hash__设为 None对象就不能放进 set 或当字典键了需要你自己再实现__hash__。这个设计是为了防止相等但哈希不同的矛盾。实践中我一般只对值对象比如金额、坐标重写这两个方法业务实体保持默认的引用比较更安全。对象的浅拷贝和深拷贝也常出问题。Python 里copy.copy(obj)只复制一层内部的 list 还是共享的改一个影响两个copy.deepcopy才是完全独立的副本。JavaScript 的{...obj}同理嵌套对象仍然是共享的需要structuredClone或手写递归。Java 里实现Cloneable接口的clone()默认也是浅拷贝。这个坑的典型症状是我改了副本原始数据也变了遇到这种诡异现象先怀疑是不是浅拷贝。4.3 我踩过的几个典型坑第一个坑是可变默认参数前面提过了但值得再强调Python 里函数和方法的所有默认参数值都是在定义时求值一次的所以列表、字典、集合这类可变对象绝对不能直接当默认值。除了用 None 兜底还有一种写法是用tuple这种不可变容器或者改用functools.lru_cache之类的显式缓存。第二个坑是子类初始化顺序。Java 里创建子类对象时父类的构造方法先执行如果父类构造方法里调用了被子类重写的方法而此时子类字段还没初始化方法里读到的就是默认值。这个问题的表现是偶尔莫名其妙拿到 null而且只在特定继承结构下出现非常难查。规避办法很简单构造方法里不要调用可以被子类重写的方法需要初始化逻辑就放到单独的init()里显式调用。第三个坑和类加载有关。Java 的静态代码块在类第一次被使用时执行且只执行一次。如果你在静态块里读了配置文件而配置又依赖某个还没初始化的类就可能拿到错误的值。排查这类问题的办法是加日志打时间戳看执行顺序。macOS 或者 Linux 上可以用-verbose:class参数打印类加载过程Windows 下同理输出会告诉你每个类被加载的时机配合断点很快能定位。第四个坑是proxy 对象的陷阱。前端做数据代理时Proxy包装过的对象和原对象不是同一个引用用比较会失败。同样的ORM 框架返回的实体对象往往是代理对象直接访问属性会触发额外的数据库查询著名的 N1 问题。这类问题的通用排查思路是打印对象的实际类型看看是不是被包装过。5. 从单类到多类协作类图、类加载与工程化落地5.1 用类图把关系理清楚再动手类写多了以后光靠脑子记关系是不行的。一个中等规模的模块往往有十几个类互相引用谁依赖谁、谁继承谁画出来才看得清。这时候类图就派上用场了。类图不需要很正式用任何能画图的工具都行把类名、关键字段、关键方法列上用箭头表示依赖方向就够了。我个人的习惯是先画数据模型实体类再画服务类最后标出哪些是接口。画的过程中经常能发现设计问题比如两个类互相依赖形成环或者某个类承担了太多职责。看别人的代码时类图同样有用。现在的 IDE 基本都支持从代码反向生成类图选中包右键就能导出一张结构图对于接手陌生项目特别高效。不过要提醒一句自动生成的图往往包含大量无关类得自己筛选。重点看继承链和接口实现关系这两条线索能帮你快速理解代码的扩展点在哪里。5.2 类加载与初始化顺序理解运行时行为类加载这个概念在 Java 里尤其重要因为它是很多奇怪现象的根源。简单说类加载分加载、链接、初始化几个阶段静态字段赋值和静态代码块属于初始化阶段只有在类被主动使用时才触发而且每个类只初始化一次。触发时机包括new 一个实例、访问静态字段、调用静态方法、反射创建、初始化子类时先初始化父类。理解这个顺序能解释不少问题。比如你写了个工具类在静态块里读环境变量第一次用是在多线程环境下就可能出现两个线程同时触发初始化虽然 JVM 保证了线程安全但如果你在静态块里做了耗时操作其他线程会被阻塞表现为第一次调用特别慢。解决办法是把耗时初始化改成懒加载用的时候再初始化。Python 的类体代码也是类似道理类定义执行时类体里的语句会立刻执行一遍所以类体里不要放有副作用的代码只放定义和常量。5.3 把类用对的几条实战经验写到这里分享几条我个人总结的经验都是被现实教育出来的。第一先写测试再写类。测试里你以调用者身份写order.add_item(米饭, 3)如果这行写起来别扭说明接口设计有问题趁早改比写完再改便宜得多。第二类名用业务词汇方法名用动词。OrderService.createOrder()比OrderManager.doCreate()可读性高一个档次命名是零成本的文档。第三一个类只做一件事如果描述它需要和字就说明该拆了。这个类负责订单创建和支付和退款——听到这种描述基本可以确定要拆成三个类。判断标准是看这个类被修改的原因有几个超过一个就该拆。第四优先组合慎用继承。继承是最强的耦合关系父类一改所有子类都要重新测试。组合只是持有引用替换实现很容易。第五不要把框架的注解和纯业务逻辑混在一起。见过不少类一半是业务计算一半是序列化注解改业务的时候得小心别动到注解。可行的做法是把核心逻辑抽到不依赖框架的类里框架层只做转换和转发这样核心逻辑可以直接单元测试跑起来也快。后面如果要继续扩展可以考虑的方向有用设计模式重构重复的条件分支用依赖注入管理类之间的依赖关系以及把领域模型和持久化模型分开。这几点每一个都能单独写一篇文章这里先埋个引子。
返回列表