BUAA_OO_Unit1 总结

jiaoziqian 学生 2024-03-22 16:38:40

面向对象设计与构造第一单元总结

前言

本单元的主体内容是表达式的化简,得益于上学期先导课程的学习,我对于工具链、Java 语法、基本的面向对象编程思想等有了一定的基础,本单元的任务就没有了过多设计以外的难度。

在这三次作业中,我初步尝试应用了抽象层次结构,对于各种因子,一开始使用了实现 接口 来统一行为,后来发现因子之间可以有一些共同的属性,因此改为继承 抽象类 。最终的层次还是有瑕疵的,我会在后面进行分析,以期将来做出更好的设计。

在整体的 架构 方面,我整体的架构比较稳定,只是在第二次作业强测后,因为出现了爆内存的问题需要重新划分部分类功能(主要是运算化简功能)。

本单元的学习中,我对于 SOLID原则 有了更深刻的理解,尤其是其中的 单一职能原则开闭原则 。由于本单元中我使用的抽象层次比较简单,其它三项原则的重要性还没有得到明显的体现,还需要我在未来的学习实践中多加体会。

测试 方面,我主要都是采用自动生成大量数据,自动运行并检验的“广撒网”式测试,并根据 hw1 的要求制作了一个简易测评机(后两个作业因为数据生成器比较复杂,而且有大佬的测评机用就没有动手更新)。这种测试方法比较“拼人品”,从测试的角度来说技术含量并不高,不能保证做到全覆盖式测试,这就导致了我 hw3 强测中出现了触发条件比较苛刻,但是其实含金量并不高的错误。以后的作业中还需要进行更多的针对性测试,提高自己的测试水平。

hw1

程序结构分析

类基础度量

类名属性个数方法个数源码行数
MainClass0113
Parser1478
InputProcessor1112
lexer.Lexer45106
lexer.Token2215
expression.Factor036
expression.VarFactor2433
expression.Expr211103
expression.NumFactor2544
expression.Term1696
ans.FinalExpr23107
ans.FinalTerm2433
ans.FullVar2215

metrics分析:

Method metrics 1


Method metrics 2


Method metrics 3

Class metrics

类图:

hw1 UML

类设计说明:

  • 默认包:
    • MainCalss -> 主类,组织各个类的工作流程。
    • InputProcesser -> 输入处理类,为了提高可拓展性从主类中独立出来,负责获取输入的原始表达式。
    • Parser -> 解析器类,负责从语法单元流中解析出表达式、项和各种因子。
  • lexer包:
    • Lexer -> 词法分析器类,负责从输入的表达式(字符串)中解析出各种语法单元并形成语法单元流。
    • Token -> 语法单元类,是 Lexer 解析的结果,提供自己的内容和类别两种属性。
  • expression包:
    • Factor -> 因子类(抽象类),提供了关于处理因子指数的一系列抽象方法,用来统一各种因子的行为,以及方便项的管理。
    • Term -> 项类,管理一系列因子,提供了增加因子、计算、化简等方法
    • Expr -> 表达式类,管理一系列项,同时本身也是一种因子,提供了增加项、计算、化简等方法
    • VarFactor -> 变量因子类,记录变量名和指数。
    • NumFactor -> 常数因子类,记录常数值和指数。
  • ans包:
    • FinalExpr -> 最终表达式类,是表达式类的化简结果,管理多个最终项,主要负责合并同类项和最终输出结果。
    • FinalTerm -> 最终项类,是项类的化简结果,记录系数和 <变量名-指数> 表,是最终表达式的基本组成部分。
    • FullVar -> 完整变量类,用于传递 <变量名-指数> 对,是为了支持多变量设计的。

优缺点分析

  • 优点:
    • 采用了递归下降的基本思路,方便后续拓展多层括号。
    • 在解析变量因子时,预留了解析多变量表达式的功能,方便后续拓展。
    • 通过预处理将多重加减号消除,只留下一个,从而避免在表达式层级记录正负,只需要将负号开头的项多乘一个值为 -1 的常数因子即可。
  • 缺点:
    • 将部分计算功能(主要是乘法和乘方)放在了用于构建语法树的表达式类和项类中,违反了单一职能原则,这也导致进行乘法运算的过程中无法完成合并同类项的操作,大大增加了时间和空间的开销。

bug 与 hack

我在本次作业的评测中没有出现 bug,也没有 hack 到别人的 bug。

主要的测试思路就是用大量样例自动测试,为此制作了一个简易评测机,包括数据生成器、正确性检验器、评测机主体三大部分,具体说明如下:

  • 数据生成器:

    • 用 Java 完成,生成数据的过程类似于解析表达式的逆过程,根据形式化定义随机生成表达式即可。

    • 在随机生成的过程中,我会记录每一个单元的 cost 并按照规则计算出总和,最终用 cost 和表达式长度对生成的数据进行筛选,保证数据的有效性,但是也一定程度上损失了强度,而且生成缓慢。

    • 实力不足,没有想到怎样增加数据的强度,只能大量生成。

      类图:

      dataGenerator

  • 正确性检验器:

    • 采用 python 搭建,总体思路就是读入原表达式和化简后的表达式,用 sympy 库实现化简和展开括号,验证化简结果是否与读入的化简结果等价,即可判断正误。

    • 值得注意的是,sympy 库的化简功能并不支持前导零,需要利用正则表达式进行预处理。此外 python 中的乘方符号与作业要求不同,需要加一步替换。

      源码:


    # Checker.py

    import re
    import ast
    from sympy import parse_expr
    from sympy import expand


    def pre(s):
        result = re.sub("[\t ]+", "", s)
        result = result.replace("^", "**")
        result = re.sub(r'\b0+(\d+)\b', r'\1', result)
        return result


    def are_expressions_equal(expr1, expr2):
        try:
            # 解析两个表达式的AST
            ast1 = ast.parse(expr1, mode='eval')
            ast2 = ast.parse(expr2, mode='eval')

            # 比较两个AST是否相同
            return ast.dump(ast1) == ast.dump(ast2)
        except (SyntaxError, ValueError):
            # 如果表达式无法解析,返回False
            return False


    def check(expr, ans_str, lines):
        expr = pre(expr)

        std_str = str(expand(parse_expr(expr)))
        print("std : " + std_str.replace(" ", "").replace("**", "^"))
        lines.append("std : " + std_str.replace(" ", "").replace("**", "^") + "\n")

        print("ans : " + ans_str.replace("\n", ""))
        lines.append("ans : " + ans_str.replace("\n", "") + "\n")
        ans_str = pre(ans_str)
        ans_str = str(parse_expr(ans_str))

        if are_expressions_equal(ans_str, std_str):
            print("AC!\n")
            lines.append("AC!\n" + "\n")
            return 0
        else:
            print("WA!\n")
            lines.append("WA!\n" + "\n")
            return -1
  • 评测机主体:

    • 采用 python 搭建,主要负责调用以上两个部分,并进行记录,负责与文件交互。

    • 主要逻辑是调用数据生成器生成指定数量的数据,然后依次调用 toHack 目录下的所有 jar 包,获取其输出并使用 Checker 进行检验,如果出错则立即终端循环并报告错误信息。

      源码:

    # MainTester.py

    import os

    import Checker


    def test(round_per_time, cur_time, total_time, targets):
        print("Getting data ...")
        os.system("java -jar data_generator.jar > data.txt " + str(round_per_time))
        print("Done")

        data = open("data.txt", "r").read().splitlines()

        lines = []

        correct = True

        for j in range(round_per_time):
            print("time " + str(cur_time) + "/" + str(total_time) + ":", end="\t")
            lines.append("time " + str(cur_time) + "/" + str(total_time) + ":" + "\t")
            print("round " + str(j + 1) + "/" + str(round_per_time) + ":")
            lines.append("round " + str(j + 1) + "/" + str(round_per_time) + ":" + "\n")
            expr = data[j * 5 + 1]
            with open('in.txt', 'w') as java_in:
                java_in.write(expr)
            for tar in targets:
                print(tar + " :")
                lines.append(tar + " :")
                os.system("java -jar toHack/" + tar + " < in.txt > out.txt ")
                with open('out.txt', 'r') as java_out:
                    ans = java_out.readline()
                if Checker.check(expr, ans, lines) == -1:
                    correct = False
                    print(tar + "ERROR!")
                    print("expr : " + expr)
                    break
            if not correct:
                break

        with open("log.txt", "w") as log:
            log.writelines(lines)

        if correct:
            print("You're so NEWBEE!\n")
            return False
        else:
            return True


    target = os.listdir("toHack")

    times = eval(input("How many times?\n(100 rounds per time)\n"))

    for i in range(times):
        if test(100, i+1, times, target):
            break

    input("press enter to continue ...")

此外,我还制作了一个可以手动输入样例并自动对 toHack 目录下的所有 jar 包进行测试的 python 程序:

# ManualHack.py

import os
import re

import Checker

ctn = True

while ctn:

    target = os.listdir("toHack")

    expr = input("your expression:\n")

    with open('in.txt', 'w') as java_in:
        java_in.write(expr)

    lines = []

    for tar in target:
        print(tar + " :")
        os.system("java -jar toHack/" + tar + " < in.txt > out.txt ")
        with open('out.txt', 'r') as java_out:
            ans = java_out.readline()
        if Checker.check(expr, ans, lines) == -1:
            print(tar + " ERROR!")
            print("expr : " + expr)
            break
        with open("log.txt", "w") as log:
            log.writelines(lines)

    ctn = re.match("[yY]", input("Enter Y to try again ...\n"))

我的优化

第一次作业的要求比较简单,因此我只做了一些结果上的优化,没有考虑计算过程的开销。

结果的优化:

最终输出的表达式可以化成统一形式:

img

基本的优化包括:

  1. 合并同类项
  2. 尽可能使用简单的输出形式(比如 '-1*' 要简化为 '-' 等)
  3. 删除所有系数为空的项,如果最后将所有的项都删空了需要补上一个0。

此外还有一个比较隐蔽的优化点(从张奕彤同学的分享贴中学到的),即“正项提前”。例如: -x+11-x 长。

为了完成这项优化,我采用的方法是:先遍历最终表达式中的所有项,找到第一个正项就先输出并 删除 该项,这导致了我的 toString 方法修改了对象本身的字段,导致 debug 时出现了意想不到的错误(因为 IDEA 的 debug 功能会自动调用对象的 toString 方法)。

hw2

程序结构分析

类基础度量

类名属性个数方法个数源码行数
MainClass0113
Parser110114
InputProcessor1117
SelfDefFuncTemp3247
lexer.Lexer45114
lexer.Token2215
std.StdPoly2688
std.StdMono310150
expression.Factor1533
expression.VarFactor1121
expression.Expr1679
expression.NumFactor1237
expression.ExpFactor1355
expression.Term1479

metrics分析:

Method metrics 1


Method metrics 2


Method metrics 3

Class metrics

类图:

hw2 UML

类设计说明:

  • 默认包:
    • MainCalss -> 主类,组织各个类的工作流程。
    • InputProcesser -> 输入处理类,为了提高可拓展性从主类中独立出来,负责获取输入的原始表达式,本次作业中新增了解析自定义函数的功能。
    • Parser -> 解析器类,负责从语法单元流中解析出表达式、项和各种因子。
    • SelfDefFuncTemp -> 自定义函数模板类,负责存储自定义函数的定义,提供参数替换功能。
  • lexer包:
    • Lexer -> 词法分析器类,负责从输入的表达式(字符串)中解析出各种语法单元并形成语法单元流。
    • Token -> 语法单元类,是 Lexer 解析的结果,提供自己的内容和类别两种属性。
  • expression包:
    • Factor -> 因子类(抽象类),方便项的管理,本次作业中将所有因子共有的指数字段放到了抽象父类中,统一了指数处理行为。
    • Term -> 项类,管理一系列因子,最终转化为标准多项式。
    • Expr -> 表达式类,管理一系列项,同时本身也是一种因子,最终要转化为标准多项式
    • VarFactor -> 变量因子类,记录变量名和指数。
    • NumFactor -> 常数因子类,记录常数值和指数。
    • ExpFactor -> 指数函数因子类,记录指数因子。
  • std包:
    • StdPoly -> 标准多项式类,管理一系列标准单项式,是表达式和项化简的结果,负责计算、化简以及最终转化为字符串。
    • StdMono -> 标准单项式类,记录系数、 <变量名-指数> 对和化简后的 e 指数,负责基本的运算和求值(转化为字符串)。

优缺点分析

  • 优点:
    • 沿用了上次作业的基本思路,没有做太大改变,保持了架构的一致性。
    • 用解析器将自定义函数的定义作为表达式进行解析,进行因子层级的替换,避免了字符串层级替换的种种问题(将 exp 的 x 换掉等)。
    • 在上一次作业的基础上进行了进一步抽象,将所有因子的指数属性、处理指数的方法都集成到抽象父类中,让整体结构更加清晰。
    • 将解析器类中解析各种因子的方法拆开,避免出现过大的方法,向“单一职能”的方向努力。
  • 缺点:
    • 抽象尚不彻底,代码中还有不少 if-instanceof 结构,这种结构属于面向对象设计“坏味道”的一种,应该用更彻底的抽象(小接口 + override)来避免它的出现。
    • 没有很好地遵守开闭原则,修改了之前已经做好的最终项(标准单项式)类,经过复盘之后我认为更好的办法应该是用“聚合”结构将已有的类聚合到新的类中。

bug 与 hack

出现的 bug

我在本次作业的强测中出现了一个数据点 内存超限 的问题,经过分析,我认为问题出在计算过程缺少优化,冗余计算太多导致需要同时管理过多的对象。

优化的方式是:

  1. 重新划分类功能(进行了一次小重构,向单一职能的方向努力)。将表达式类和项类的计算功能移除,让它们专门用来构建语法树,转而将所有的计算功能都划归到标准单项式、多项式(原来的最终项、表达式)中。这样修改之后,可以做到在做乘法之前先合并同类项,减少需要计算的单项式数量。
  2. 计算方法优化。在做乘法的时候,如果有一个因子值为0则直接结束乘法过程,返回0;计算过程中就删除合并同类项后系数为0的项。

hack 策略

我的 hack 策略主要是两方面结合:一方面通过大量样例批量化测试,另一方面则是从特殊样例出发,主要针对格式进行攻击,包括指数不能为负、"-x" 不是单一因子、计算结果为0不能输出空串等。

我的优化

本次作业中,计算性能上的优化已经在 bug 部分说过了,结果上的优化做得比较保守,除了与上次相同的几点基本优化之外,只做了一个很保守的优化:用正则表达式,将形如 "exp((a*x))" 的式子替换为 "exp(x)^a"(其中 a 为正整数)。

替换代码:

ans = ans.replaceAll("(exp\\(\\()([0-9]+)(\\*)(x\\^?\\d*)(\\)\\))", "exp($4)^$2");

此外,完成本次作业的过程中,由于明白了在 toString 方法中修改对象内字段会导致各种意外,我修改了对象管理方式和求值方法,平时用 HashSet 管理内部的标准多项式,然后在 toString 方法中用该集合创建一个 ArrayList,并按系数进行排序。

关键代码:

// ...

public class StdPoly {
    private static final Comparator<StdMono> cmp = (o1, o2) -> o2.getCoe().compareTo(o1.getCoe());

    // ... 

    @Override
    public String toString() {
        ArrayList<StdMono> stdMonoList = new ArrayList<>(stdMonoSet);
        stdMonoList.sort(cmp);
        // ...
    }
}

hw3

程序结构分析

类基础度量

类名属性个数方法个数源码行数
MainClass0113
Parser111126
InputProcessor1117
SelfDefFuncTemp3247
lexer.Lexer45117
lexer.Token2215
std.StdPoly211145
std.StdMono413190
expression.Factor1535
expression.VarFactor1121
expression.DerFactor1125
expression.Expr1570
expression.NumFactor1244
expression.ExpFactor1355
expression.Term1479

metrics分析:

Method metrics 1


Method metrics 2


Method metrics 3


Method metrics 4

Class metrics

类图:

hw3 UML

类设计说明:

  • 默认包:
    • MainCalss -> 主类,组织各个类的工作流程。
    • InputProcesser -> 输入处理类,为了提高可拓展性从主类中独立出来,负责获取输入的原始表达式和解析自定义函数。
    • Parser -> 解析器类,负责从语法单元流中解析出表达式、项和各种因子。
    • SelfDefFuncTemp -> 自定义函数模板类,负责存储自定义函数的定义,提供参数替换功能。
  • lexer包:
    • Lexer -> 词法分析器类,负责从输入的表达式(字符串)中解析出各种语法单元并形成语法单元流。
    • Token -> 语法单元类,是 Lexer 解析的结果,提供自己的内容和类别两种属性。
  • expression包:
    • Factor -> 因子类(抽象类),方便项的管理,统一处理指数相关操作。
    • Term -> 项类,管理一系列因子,最终转化为标准多项式。
    • Expr -> 表达式类,管理一系列项,同时本身也是一种因子,最终要转化为标准多项式
    • VarFactor -> 变量因子类,记录变量名和指数。
    • DerFactor -> 求导因子类,记录待求导的表达式(求值时先转化为标准多项式)。
    • NumFactor -> 常数因子类,记录常数值和指数。
    • ExpFactor -> 指数函数因子类,记录指数因子。
  • std包:
    • StdPoly -> 标准多项式类,管理一系列标准单项式,是表达式和项化简的结果,负责计算、化简以及最终转化为字符串。
    • StdMono -> 标准单项式类,记录系数、 <变量名-指数> 对和化简后的 e 指数,负责基本的运算和求值(转化为字符串),本次作业中新增了求导功能,对标准形式的单项式进行求导。

标准单项式:

img

优缺点分析

  • 优点:
    • 架构相对稳定,解析函数定义式并做因子层级替换的做法可以直接支持函数定义式调用已定义函数。
    • 由于标准单项式类的存在,不需要对表达式、项、各种因子分别设计求导方法,只需要对标准形式求导即可。
  • 缺点:
    • 仍然有许多 if-instanceof 等“坏味道”存在,保有过多分支会导致一些触发条件相对严格的 bug 不容易被测试出来。

bug 与 hack

出现的 bug

本次作业需要修改的量并不大,但是由于粗心出现了两个很简单但是比较隐蔽的小错误,由于测试不彻底没有被发现,最终在强测暴雷了。

  1. 我在新建的类,求导因子类的拷贝构造方法中没有拷贝指数,导致在拷贝求导因子对象时指数会丢失。
  2. 在记录求导因子的求导对象时,我的做法是遇到 "dx(dx(expr))" 时会认为是对 expr 求二阶导,但是这样会导致我忽略 "dx(dx(expr1) + expr2)" 这种情况,因此其实并不需要记录几阶导数,只需要递归求导即可。

hack 策略

本次作业的 hack 策略与上次类似,“广撒网”和针对格式攻击。

我的针对性测试能力尚且大大不足,可能还是有很多触发条件较为苛刻的 bug 测不出来,而且也没有针对递归层数、内存或时间限制进行测试,这方面能力有待提升。

我的优化

本次作业的计算过程优化没有变化,结果长度优化则仍然比较保守,增加了针对所有 exp() 括号中所有系数绝对值相等的情况的提公因式,因为担心负优化,没有做更多的提公因式和拆分(也是因为码力不足)。

后来经过反思,其实还可以加一条提最大公因式,然后比较提取前后的长度,如果出现负优化,取消优化即可。

架构设计体验

迭代体验

第一次作业中,我猜测后期迭代中会加入多变元的情况,因此在变量因子中,预留了变量名不同的可能性,并在最终项中用列表存储变量因子,显得很不优雅,最后这个设计部分地派上了用场,即可以用来解析自定义函数定义式,但是最终的结果中并不会有多种变量,因此我在第二次作业中就将变量列表删除了。

此外,我在第二次作业中修改最终输出模式时,采用的方法是求改原有的单项式模板,而不是新建类并进行聚合,违反了开闭原则,由于我们的迭代次数较少,且只改变了一次输出形式(增加exp),所以影响不大,但是这三次作业我已经很明显地感觉到记不住自己一开始设计某个类的用意了,如果以后更大的项目中还这样修改而不是新增的话很有可能会增加大量的工作量并且会埋下一些隐患。

总之,今后的设计一定要尽可能遵循 SOLID 原则,让“自己和自己的合作”更加顺畅。

预设新的迭代情景

比如说我们未来需要新增三角函数因子,则需要做一下四个修改:

  1. 增加 sin,cos等语法单位;
  2. 在解析器中加入解析三角函数的方法;
  3. 新建一个标准单项式类,其中包含一个属性,即原本的标准单项式,然后再加入记录三角函数内部因子的属性;
  4. 在标准单项式、多项式类中增加针对三角函数的乘法和合并同类项功能,然后酌情考虑诱导公式等优化策略。

新知记录:使用原生 Java 实现深拷贝的三种方法

本次作业的运算过程中,我们需要很多对对象的拷贝,因为我们有多层自定义类对象的嵌套,如果只是浅拷贝就会导致运算出错,所以我们需要进行深拷贝。

那么,在不依赖第三方包的情况下,我们都有哪些方法可以完成这一任务呢?

方法一:拷贝构造

为需要拷贝的类新增一个拷贝构造函数,即设计一个传入参数为本类对象的构造函数,然后将自身的所有属性都设置为参数对应的属性(遇到引用类型需要调用属性的拷贝构造方法)。

  • 优点:
    • 实现简单直观
    • 不需要依赖额外的接口
  • 缺点:
    • 成员变量发生变动需要修改方法,不满足开闭原则
    • 不具有可复用性

方法二:重写 Object 类的 clone 方法

  • 优点:
    • 较方式1实现更简单,不需要关注copy细节;
    • 不修改引用类型成员变量不需要修改代码
  • 缺点:
    • 需要实现Cloneable,重写父类clone方法,不满足里式替换;
    • 引用类型成员变量发生变动需要修改方法,不满足开闭原则;
    • 不具有可复用性;

方法三:对象处理流(序列化与反序列化)

序列化就是保存数据的时候,保存数据的值和数据类型

反序列化就是在恢复数据时,恢复数据的值和数据类型

需要让某个对象支持序列化机制,则必须让其类是可序列化的。为了让某个类是可序列化的,该类必须实现如下两个接口之一(他的每个属性也必须实现这两个接口之一)

  • Serializable

  • Externalizable

我们用序列化反序列化实现 clone 的原理就是我们相当于是把对象保存到了一个文件中,然后立刻又从文件中把这个对象读了出来,此时这个新的对象就是一个深度 clone 的副本了。

标准写法:

public class SerialCloneable implements Cloneable,Serializable  
{  
    public Object clone()  
    {  
        try  
        {  
            //save the object to a byte array  
            ByteArrayOutputStream bout = new ByteArrayOutputStream();  
            ObjectOutputStream out = new ObjectOutputStream(bout);  
            out.writeObject(this);  
            out.close();  

            //read a clone of the object from the byte array  
            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());  
            ObjectInputStream in = new ObjectInputStream(bin);  
            Object result = in.readObject();  
            in.close();  

            return result;  
        }
        catch(Exception e)  
        {  
            return null;    
        }  
    }  

}

——以上来自学长博客“钟鼓楼”

  • 优点:
    • 不破坏类的封装,无需了解被copy对象的内部
    • 不需要依赖第三方包
    • 代码可复用
  • 缺点:
    • 需要实现Serializable接口,会有额外的开销

参考资料:

  1. https://www.cnblogs.com/qxrblog/p/15013624.html
  2. https://thysrael.github.io/posts/8da51baf/

心得体会

在本单元的学习和实践中,我意识到自己在面向对象设计方面还有更多需要学习的知识。本来觉得经过上学期先导课的训练,我已经对 OO 有了比较完备的理解,但是这单元的学习让我意识到我的代码中还有很多“不 OO”的元素存在,这需要我在未来的学习实践中更多地学习他人的架构,从学长的博客和讨论区多多吸收知识,不必一拿到题目想一想就开始动手写代码,可以在自己的腹稿初步成型后多多吸取别人的优秀想法,博采众长。

另外,我觉得荣老师上课经常强调的一个思想很有道理,即虽然你是在一个人完成整个工程,但应该认为是“自己雇佣自己”,即规定好每个模块的接口之后完全模块化设计,模拟一种合作式开发的感觉,我想这对于我们的代码向“高内聚,低耦合”方向发展很有好处,也利于我们未来参与多人合作,开发更大的项目。

未来方向

如果可能的话希望每个强测点能有一个标准优化解,如果性能比这个解还要好计入加分而不是对别的同学扣分。

...全文
92 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧