2.1 C# 基础语法 (变量, 数据类型, 控制流, 函数)


文档摘要

2.1 C# 基础语法 (变量, 数据类型, 控制流, 函数) 二、C# 脚本编程父章节领域 2.1 C# 基础语法 (变量, 数据类型, 控制流, 函数) 为什么 C# 基础语法在 Unity 中如此重要? Unity 引擎主要使用 C# 作为其脚本语言。理解 C# 基础语法意味着您能够: 控制游戏对象: 通过脚本操纵游戏对象的位置、旋转、缩放、材质等属性。 创建游戏逻辑: 实现游戏规则、玩家互动、AI 行为等复杂的逻辑。 与 Unity API 交互: 利用 Unity 提供的强大 API(应用程序编程接口)来访问引擎的各种功能,例如物理引擎、动画系统、UI 系统等。 编写可维护的代码: 良好的语法基础能够帮助您编写结构清晰、易于理解和维护的代码,从而提高开发效率和代码质量。

2.1 C# 基础语法 (变量, 数据类型, 控制流, 函数)

二、C# 脚本编程父章节领域

2.1 C# 基础语法 (变量, 数据类型, 控制流, 函数)

为什么 C# 基础语法在 Unity 中如此重要?

Unity 引擎主要使用 C# 作为其脚本语言。理解 C# 基础语法意味着您能够:

  • 控制游戏对象: 通过脚本操纵游戏对象的位置、旋转、缩放、材质等属性。

  • 创建游戏逻辑: 实现游戏规则、玩家互动、AI 行为等复杂的逻辑。

  • 与 Unity API 交互: 利用 Unity 提供的强大 API(应用程序编程接口)来访问引擎的各种功能,例如物理引擎、动画系统、UI 系统等。

  • 编写可维护的代码: 良好的语法基础能够帮助您编写结构清晰、易于理解和维护的代码,从而提高开发效率和代码质量。

让我们逐步深入 C# 的基础语法世界。

2.1.1 变量 (Variables)

变量可以被认为是存储数据的容器。在编程中,我们经常需要存储各种类型的信息,例如玩家的生命值、游戏对象的位置、物品的名称等等。变量允许我们为这些数据命名,并在程序的执行过程中访问和修改它们。

变量的声明和初始化

在使用变量之前,您需要先声明它。声明变量意味着告诉编译器您要创建一个特定类型的变量,并为其命名。C# 中变量声明的基本语法如下:

数据类型 变量名;

例如,如果您想声明一个存储整数的变量,可以这样做:

int playerHealth;

这行代码声明了一个名为 playerHealth 的变量,它的数据类型是 int(整数)。

声明变量后,您可以对其进行初始化,即为变量赋予一个初始值。您可以在声明变量的同时进行初始化,也可以在稍后的代码中进行初始化。

// 声明并初始化变量 int playerScore = 0; // 先声明变量,稍后初始化 float moveSpeed; moveSpeed = 5.0f;

变量的命名规范

为了使代码更易读和维护,我们应该遵循一些变量命名规范:

  • 使用有意义的名字: 变量名应该清晰地表达变量所存储的数据的含义。例如,playerNamepn 更具可读性。

  • 使用驼峰命名法 (camelCase): 这是 C# 中常用的命名规范。驼峰命名法指的是变量名由多个单词组成时,除了第一个单词外,后续单词的首字母大写。例如,playerMoveSpeedcurrentScore

  • 避免使用保留字: C# 中有一些保留字(关键字),例如 intfloatclass 等,不能用作变量名。

  • 变量名区分大小写: playerHealthPlayerHealth 是不同的变量。

变量的作用域 (Scope)

变量的作用域指的是变量在代码中可以被访问和使用的范围。变量的作用域由其声明的位置决定。

  • 局部变量 (Local Variables): 在方法、函数或代码块内部声明的变量是局部变量。局部变量只能在其声明的代码块内部访问。
void Update() { // gameObjectName 是局部变量,只能在 Update() 方法中使用 string gameObjectName = gameObject.name; Debug.Log("当前游戏对象名称: " + gameObjectName); }
  • 成员变量 (Member Variables) / 字段 (Fields): 在类 (class) 的内部,方法之外声明的变量是成员变量或字段。成员变量属于类的实例,可以在类的任何方法中访问。在 Unity 中,我们通常在继承自 MonoBehaviour 的脚本中使用成员变量来存储游戏对象的属性和状态。
public class PlayerController : MonoBehaviour { // playerSpeed 是成员变量,可以在 PlayerController 类的任何方法中使用 public float playerSpeed = 10.0f; void Update() { // 可以访问成员变量 playerSpeed transform.Translate(Vector3.forward * playerSpeed * Time.deltaTime); } }

代码实践:Unity 中的变量应用

在 Unity 中,我们经常使用变量来引用游戏对象、组件和各种属性。

using UnityEngine; public class VariableExample : MonoBehaviour { // 公有变量,可以在 Unity 编辑器中直接设置 public GameObject targetObject; public float rotationSpeed = 30.0f; public bool isRotating = true; private Vector3 initialPosition; // 私有变量,只能在当前脚本中访问 void Start() { // 初始化变量 initialPosition = transform.position; // 检查是否成功引用了目标对象 if (targetObject == null) { Debug.LogError("请在 Inspector 面板中为 targetObject 变量赋值!"); } } void Update() { if (isRotating && targetObject != null) { // 获取目标对象的 Transform 组件 Transform targetTransform = targetObject.GetComponent<Transform>(); if (targetTransform != null) { // 让目标对象绕 Y 轴旋转 targetTransform.Rotate(Vector3.up * rotationSpeed * Time.deltaTime); } } } }

代码解释:

  • public GameObject targetObject;:声明一个公有的 GameObject 类型的变量 targetObjectpublic 关键字使得这个变量可以在 Unity 编辑器的 Inspector 面板中显示,允许您在编辑器中直接拖拽游戏对象赋值给它。

  • public float rotationSpeed = 30.0f;:声明一个公有的 float 类型的变量 rotationSpeed 并初始化为 30.0f。

  • public bool isRotating = true;:声明一个公有的 bool 类型的变量 isRotating 并初始化为 true

  • private Vector3 initialPosition;:声明一个私有的 Vector3 类型的变量 initialPositionprivate 关键字使得这个变量只能在 VariableExample 脚本内部访问,无法在 Inspector 面板中显示,也无法被其他脚本直接访问。

  • initialPosition = transform.position;:在 Start() 方法中,将当前游戏对象的位置赋值给 initialPosition 变量。

  • if (targetObject == null):检查 targetObject 是否为空(null),如果为空,则输出错误信息到 Unity 控制台。

  • Transform targetTransform = targetObject.GetComponent<Transform>();:获取 targetObjectTransform 组件,并将其赋值给 targetTransform 变量。

  • targetTransform.Rotate(Vector3.up * rotationSpeed * Time.deltaTime);:让 targetTransform 绕 Y 轴旋转,旋转速度由 rotationSpeed 控制,并使用 Time.deltaTime 来确保旋转速度与帧率无关。

2.1.2 数据类型 (Data Types)

数据类型定义了变量可以存储的数据种类以及可以对这些数据执行的操作。C# 是一种强类型语言,这意味着您必须在声明变量时指定其数据类型,并且变量只能存储与其声明类型兼容的数据。

C# 中主要的数据类型可以分为以下几类:

1. 值类型 (Value Types)

值类型直接存储数据的值。当您将一个值类型变量赋值给另一个值类型变量时,会复制实际的值。C# 中常用的值类型包括:

  • 整型 (Integer Types): 用于存储整数。

    • int: 32 位整数,范围约为 -20 亿 到 +20 亿。

    • long: 64 位整数,范围更大。

    • short: 16 位整数,范围较小。

    • byte: 8 位无符号整数,范围 0 到 255。

  • 浮点型 (Floating-Point Types): 用于存储带有小数部分的数字。

    • float: 32 位单精度浮点数。在 Unity 中常用,通常在数值后面加上 fF 后缀,例如 3.14f

    • double: 64 位双精度浮点数,精度更高,但占用更多内存。

  • 布尔型 (Boolean Type): 用于存储逻辑值,只能是 truefalse

    • bool: 表示真或假。
  • 字符型 (Character Type): 用于存储单个字符。

    • char: 16 位 Unicode 字符,用单引号括起来,例如 'A''1''$'
  • 枚举 (Enum Type): 用户自定义的类型,用于表示一组命名的常量。

2. 引用类型 (Reference Types)

引用类型不直接存储数据的值,而是存储对数据存储位置的引用(内存地址)。当您将一个引用类型变量赋值给另一个引用类型变量时,只会复制引用,而不是实际的数据。这意味着两个变量将指向同一个数据。C# 中常用的引用类型包括:

  • 字符串 (String Type): 用于存储文本。

    • string: 表示一串字符,用双引号括起来,例如 "Hello World"
  • 数组 (Array Type): 用于存储相同类型元素的集合。

  • 类 (Class Type): 用户自定义的类型,是面向对象编程的核心概念。Unity 中的 GameObjectTransformComponent 等都是类。

  • 接口 (Interface Type): 定义一组方法和属性的契约。

  • 委托 (Delegate Type): 表示方法的引用。

3. Unity 特有的数据类型

Unity 提供了一些专门用于游戏开发的数据类型,这些类型在 UnityEngine 命名空间下。常用的 Unity 数据类型包括:

  • Vector2: 二维向量,用于表示 2D 坐标、方向等。

  • Vector3: 三维向量,用于表示 3D 坐标、方向、力等。

  • Vector4: 四维向量,用于表示颜色、齐次坐标等。

  • Quaternion: 四元数,用于表示旋转,比欧拉角更稳定,避免万向节锁问题。

  • Color: 颜色,由红、绿、蓝、透明度 (Alpha) 分量组成。

  • GameObject: 游戏对象,Unity 场景中的基本构建块。

  • Transform: 变换组件,控制游戏对象的位置、旋转、缩放。

  • Material: 材质,定义游戏对象的外观。

  • Texture: 纹理,用于给游戏对象表面添加图像。

值类型 vs. 引用类型

理解值类型和引用类型之间的区别至关重要。

特性 值类型 (Value Type) 引用类型 (Reference Type)
存储内容 实际数据值 数据的引用 (内存地址)
内存分配 栈 (Stack) 堆 (Heap)
赋值行为 复制值 复制引用
默认值 每个值类型都有默认值 (例如 int 的默认值是 0, bool 的默认值是 false) 默认值为 null (表示不引用任何对象)
示例 int, float, bool, struct, enum string, class, array, interface, delegate

类型转换 (Type Conversion)

在编程中,有时需要在不同数据类型之间进行转换。C# 提供了两种主要的类型转换方式:

  • 隐式转换 (Implicit Conversion): 当从较小范围的数据类型转换为较大范围的数据类型时,编译器会自动进行隐式转换,不会丢失数据。例如,从 int 转换为 floatdouble
int integerValue = 10; float floatValue = integerValue; // 隐式转换,int 转换为 float Debug.Log(floatValue); // 输出 10
  • 显式转换 (Explicit Conversion) / 类型强制转换 (Casting): 当从较大范围的数据类型转换为较小范围的数据类型时,或者在类型之间没有直接的隐式转换关系时,需要进行显式转换。显式转换需要使用强制类型转换运算符 ()。显式转换可能会导致数据丢失或精度降低。
float floatValue = 3.14f; int integerValue = (int)floatValue; // 显式转换,float 转换为 int,小数部分被截断 Debug.Log(integerValue); // 输出 3 double doubleValue = 123.456; int anotherIntegerValue = (int)doubleValue; // 显式转换,double 转换为 int,小数部分被截断 Debug.Log(anotherIntegerValue); // 输出 123
  • 类型转换方法 (Type Conversion Methods): C# 提供了一些静态类和方法来进行更安全的类型转换,例如 Convert 类和 Parse 方法。
string stringNumber = "123"; int parsedInteger = int.Parse(stringNumber); // 使用 int.Parse 将字符串转换为 int Debug.Log(parsedInteger); // 输出 123 string stringFloat = "3.14"; float parsedFloat = float.Parse(stringFloat); // 使用 float.Parse 将字符串转换为 float Debug.Log(parsedFloat); // 输出 3.14 string stringBoolean = "true"; bool parsedBoolean = bool.Parse(stringBoolean); // 使用 bool.Parse 将字符串转换为 bool Debug.Log(parsedBoolean); // 输出 True int number = 456; string convertedString = Convert.ToString(number); // 使用 Convert.ToString 将 int 转换为 string Debug.Log(convertedString); // 输出 "456"

代码实践:Unity 中数据类型的应用

using UnityEngine; public class DataTypeExample : MonoBehaviour { public int healthPoints = 100; public float moveSpeed = 5.0f; public string playerName = "PlayerOne"; public bool isGameOver = false; public Vector3 playerPosition = Vector3.zero; public Color playerColor = Color.red; void Start() { Debug.Log("玩家名称: " + playerName); Debug.Log("初始生命值: " + healthPoints); Debug.Log("移动速度: " + moveSpeed); Debug.Log("是否游戏结束: " + isGameOver); Debug.Log("初始位置: " + playerPosition); Debug.Log("玩家颜色: " + playerColor); // 修改变量的值 healthPoints -= 20; moveSpeed += 1.5f; isGameOver = true; playerPosition = new Vector3(1, 2, 3); playerColor = Color.blue; Debug.Log("--- 修改后的属性 ---"); Debug.Log("当前生命值: " + healthPoints); Debug.Log("当前移动速度: " + moveSpeed); Debug.Log("是否游戏结束: " + isGameOver); Debug.Log("当前位置: " + playerPosition); Debug.Log("玩家颜色: " + playerColor); } }

代码解释:

  • 代码中声明了各种不同数据类型的变量,包括 intfloatstringboolVector3Color,并赋予了初始值。

  • Start() 方法中,使用 Debug.Log() 函数将这些变量的值输出到 Unity 控制台。

  • 随后,代码修改了部分变量的值,并再次输出修改后的值,演示了变量值的动态变化。

2.1.3 控制流 (Control Flow)

控制流语句允许您控制程序代码的执行顺序。通过控制流语句,您可以根据条件执行不同的代码块,或者重复执行某些代码块。C# 中主要的控制流语句包括:

1. 条件语句 (Conditional Statements)

条件语句允许您根据条件判断的结果来执行不同的代码块。

  • if 语句: if 语句是最基本的条件语句。如果 if 语句的条件为真 (true),则执行 if 代码块中的代码。
graph TD A[开始] --> B{条件表达式}; B -- true --> C[执行 if 代码块]; B -- false --> D[跳过 if 代码块]; C --> E[继续执行后续代码]; D --> E;
int score = 75; if (score >= 60) { Debug.Log("及格了!"); }
  • if-else 语句: if-else 语句在 if 条件为假 (false) 时,执行 else 代码块中的代码。
graph TD A[开始] --> B{条件表达式}; B -- true --> C[执行 if 代码块]; B -- false --> D[执行 else 代码块]; C --> E[继续执行后续代码]; D --> E;
int health = 30; if (health > 0) { Debug.Log("玩家还活着!"); } else { Debug.Log("玩家死亡!"); }
  • if-else if-else 语句: if-else if-else 语句允许您检查多个条件。它首先检查 if 条件,如果为假,则检查第一个 else if 条件,以此类推。如果所有 ifelse if 条件都为假,则执行 else 代码块中的代码(如果有 else 代码块)。
graph TD A[开始] --> B{条件表达式 1}; B -- true --> C[执行 if 代码块]; B -- false --> D{条件表达式 2}; D -- true --> E[执行 else if 代码块 1]; D -- false --> F{条件表达式 3}; F -- true --> G[执行 else if 代码块 2]; F -- false --> H[执行 else 代码块 (可选)]; C --> I[继续执行后续代码]; E --> I; G --> I; H --> I;
int score = 85; if (score >= 90) { Debug.Log("优秀!"); } else if (score >= 80) { Debug.Log("良好!"); } else if (score >= 60) { Debug.Log("及格!"); } else { Debug.Log("不及格!"); }
  • switch 语句: switch 语句根据表达式的值,从多个 case 分支中选择一个执行。switch 语句通常用于处理多个离散的条件分支,比多个 if-else if 语句更简洁易读。
graph TD A[开始] --> B{表达式}; B --> C{case 值 1}; B --> D{case 值 2}; B --> E{case 值 3}; C -- 匹配 --> F[执行 case 1 代码块]; D -- 匹配 --> G[执行 case 2 代码块]; E -- 匹配 --> H[执行 case 3 代码块]; B -- 都不匹配 --> I[执行 default 代码块 (可选)]; F --> J[继续执行后续代码]; G --> J; H --> J; I --> J;
string weaponType = "Sword"; switch (weaponType) { case "Sword": Debug.Log("装备了剑!"); break; // 每个 case 分支必须以 break 语句结束,除非是空的 case 分支 case "Bow": Debug.Log("装备了弓!"); break; case "Staff": Debug.Log("装备了法杖!"); break; default: Debug.Log("装备了未知武器!"); break; }

2. 循环语句 (Loop Statements)

循环语句允许您重复执行一段代码块,直到满足某个条件为止。

  • for 循环: for 循环通常用于在已知循环次数的情况下重复执行代码。for 循环包含三个部分:初始化表达式、条件表达式和迭代表达式。
graph TD A[开始] --> B[初始化表达式]; B --> C{条件表达式}; C -- true --> D[执行循环体代码]; D --> E[迭代表达式]; E --> C; C -- false --> F[跳出循环]; F --> G[继续执行后续代码];
for (int i = 0; i < 5; i++) { Debug.Log("当前计数器 i 的值: " + i); }
  • while 循环: while 循环在条件为真 (true) 的情况下重复执行代码块。while 循环在循环次数未知的情况下非常有用。
graph TD A[开始] --> B{条件表达式}; B -- true --> C[执行循环体代码]; C --> B; B -- false --> D[跳出循环]; D --> E[继续执行后续代码];
int count = 0; while (count < 3) { Debug.Log("当前计数 count 的值: " + count); count++; // 记得在循环体内更新循环条件,否则可能导致无限循环 }
  • do-while 循环: do-while 循环与 while 循环类似,但 do-while 循环会先执行一次循环体代码,然后再检查条件。这意味着 do-while 循环至少会执行一次。
graph TD A[开始] --> B[执行循环体代码]; B --> C{条件表达式}; C -- true --> B; C -- false --> D[跳出循环]; D --> E[继续执行后续代码];
int number = 5; do { Debug.Log("当前 number 的值: " + number); number--; } while (number > 0);
  • foreach 循环: foreach 循环用于遍历集合(例如数组、列表)中的每个元素。foreach 循环简化了遍历集合的操作。
string[] colors = {"Red", "Green", "Blue"}; foreach (string color in colors) { Debug.Log("颜色: " + color); }

3. 跳转语句 (Jump Statements)

跳转语句用于改变程序的执行流程,立即跳转到代码的另一个位置。

  • break 语句: break 语句用于立即跳出循环(forwhiledo-whileforeach)或 switch 语句。
for (int i = 0; i < 10; i++) { if (i == 5) { break; // 当 i 等于 5 时,跳出 for 循环 } Debug.Log("当前 i 的值: " + i); }
  • continue 语句: continue 语句用于跳过当前循环迭代的剩余代码,并立即开始下一次循环迭代。
for (int i = 0; i < 5; i++) { if (i == 2) { continue; // 当 i 等于 2 时,跳过本次循环迭代的剩余代码,直接进入下一次迭代 } Debug.Log("当前 i 的值: " + i); }

代码实践:Unity 中控制流的应用

using UnityEngine; public class ControlFlowExample : MonoBehaviour { public float moveSpeed = 5.0f; public float rotationSpeed = 90.0f; public KeyCode moveForwardKey = KeyCode.W; public KeyCode rotateLeftKey = KeyCode.A; public KeyCode rotateRightKey = KeyCode.D; void Update() { // 条件语句:根据按键输入控制移动 if (Input.GetKey(moveForwardKey)) { transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime); } // 条件语句:根据按键输入控制旋转 if (Input.GetKey(rotateLeftKey)) { transform.Rotate(Vector3.down * rotationSpeed * Time.deltaTime); } else if (Input.GetKey(rotateRightKey)) { transform.Rotate(Vector3.up * rotationSpeed * Time.deltaTime); } // 循环语句:模拟物体周期性颜色变化 float time = Time.time; float cycleTime = 2.0f; // 颜色变化周期为 2 秒 float normalizedTime = Mathf.Repeat(time, cycleTime) / cycleTime; // 将时间限制在 0-1 之间循环 Color lerpedColor = Color.Lerp(Color.red, Color.blue, normalizedTime); // 在红色和蓝色之间插值颜色 GetComponent<Renderer>().material.color = lerpedColor; } }

代码解释:

  • if (Input.GetKey(moveForwardKey)): 使用 if 语句检查是否按下了前进按键(默认为 'W' 键)。如果按下,则使用 transform.Translate() 方法向前移动游戏对象。

  • if (Input.GetKey(rotateLeftKey)) ... else if (Input.GetKey(rotateRightKey)): 使用 if-else if 语句检查是否按下了左旋转按键('A' 键)或右旋转按键('D' 键)。根据按键输入,使用 transform.Rotate() 方法旋转游戏对象。

  • float normalizedTime = Mathf.Repeat(time, cycleTime) / cycleTime;: 使用 Mathf.Repeat() 函数和除法运算,将 Time.time 限制在 0-1 之间循环,实现周期性的时间变化。

  • Color lerpedColor = Color.Lerp(Color.red, Color.blue, normalizedTime);: 使用 Color.Lerp() 函数在红色 (Color.red) 和蓝色 (Color.blue) 之间根据 normalizedTime 进行颜色插值,实现颜色渐变效果。

  • GetComponent<Renderer>().material.color = lerpedColor;: 将插值得到的颜色赋值给游戏对象的 Renderer 组件的 material.color 属性,从而改变游戏对象的颜色。

2.1.4 函数 (Functions)

函数是一段组织好的、可重复使用的代码块,用于执行特定的任务。函数可以接受输入参数(称为参数),并可以返回一个值。使用函数可以将代码模块化,提高代码的可重用性和可读性。

函数的声明和定义

函数的声明和定义包括以下几个部分:

  • 访问修饰符 (Access Modifier): 例如 publicprivateprotected 等,用于控制函数的访问权限。

  • 返回类型 (Return Type): 指定函数返回值的类型。如果函数不返回任何值,则返回类型为 void

  • 函数名 (Function Name): 函数的名称,遵循与变量命名类似的规范。

  • 参数列表 (Parameter List): 可选的参数列表,用于接收输入参数。参数列表包含参数类型和参数名,多个参数之间用逗号分隔。

  • 函数体 (Function Body): 包含函数要执行的代码块,用花括号 {} 括起来。

访问修饰符 返回类型 函数名(参数列表) { // 函数体 // ... return 返回值; // 如果返回类型不是 void,则需要使用 return 语句返回值 }

函数调用 (Function Call)

要执行函数中的代码,需要调用函数。函数调用使用函数名后跟括号 (),如果函数有参数,则需要在括号中传递参数值(称为实参)。

函数名(实参列表);
graph TD A[主程序] --> B[函数调用]; B --> C[进入函数]; C --> D[执行函数体代码]; D --> E[返回结果 (如果函数有返回值)]; E --> F[返回主程序];

示例:一个简单的函数

// 定义一个名为 Add 的函数,接受两个 int 类型参数,返回 int 类型的值 public int Add(int a, int b) { int sum = a + b; return sum; // 返回两个参数的和 } // 在其他代码中调用 Add 函数 int result = Add(5, 3); // 调用 Add 函数,传入实参 5 和 3,并将返回值赋值给 result 变量 Debug.Log("5 + 3 = " + result); // 输出 "5 + 3 = 8"

函数的类型

  • void 函数: void 函数不返回任何值。它们主要用于执行某些操作,例如打印信息、修改变量的值等。
public void PrintMessage(string message) { Debug.Log("消息: " + message); } PrintMessage("Hello from void function!"); // 调用 void 函数
  • 带返回值的函数: 带返回值的函数会返回一个特定类型的值。您可以使用 return 语句来指定函数返回的值。
public float CalculateArea(float radius) { float area = Mathf.PI * radius * radius; return area; // 返回计算得到的圆的面积 } float circleArea = CalculateArea(2.5f); // 调用带返回值的函数,并将返回值赋值给 circleArea 变量 Debug.Log("圆的面积: " + circleArea);

参数 (Parameters) 和实参 (Arguments)

  • 参数 (Parameters): 在函数声明中定义的变量,用于接收函数调用时传递的输入值。参数就像函数的占位符,用于接收外部数据。

  • 实参 (Arguments): 在函数调用时传递给函数的实际值。实参会被赋值给函数声明中对应的参数。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U