# CSharp 综合练习 01 ## 说明 - 本题单依据你当前已有笔记整理,范围主要来自: - `基本概念` - `入门:常量、转义字符、格式转换、字符串拼接、三目运算符` - `基础:值类型和引用类型、ref 和 out、变长参数、枚举` - `核心:继承、静态成员、成员属性、索引器` - 你可以直接在每道题下方的 `你的作答` 区域填写答案。 - 我后续会在 `批改区` 直接给你批改和点评。 --- ## 入门 ### 1. [填空题] 面向对象基础概念 根据你的笔记,完成下面填空: 1. 面向对象三大特性分别是:`____`、`____`、`____` 2. “对扩展开放,对修改关闭”指的是 `____原则` 3. “优先使用组合,而不是为了复用就滥用继承”指的是 `____原则` **你的作答** 1.继承、封装、多态 2.开闭原则 3.合成复用原则 **批改区** 判定:正确。 点评:三大特性、开闭原则、合成复用原则都答对了。第 1 小题的顺序不影响正确性,更常见的写法是“封装、继承、多态”。 ### 2. [简答题] const 根据 `常量.md`,回答下面问题: 1. `const` 声明的变量有哪些核心特点? 2. 请写出两个适合使用 `const` 的场景。 **你的作答** 1.不能修改、必须初始化 2.定义PI、重力加速度等需要某个常量的时候 **批改区** 判定:正确。 点评:你抓住了 `const` 的两个核心点:必须初始化、之后不能修改;场景举例也合适,比如 `PI`、重力加速度这类固定值。 ### 3. [代码阅读题] 转义字符 阅读下面代码,写出三次输出的大致结果,并说明分别用到了哪些转义字符。 ```csharp string title = "\"C#入门\""; string path = "C:\\Users\\Kister"; string text = "第一行\n第二行"; Console.WriteLine(title); Console.WriteLine(path); Console.WriteLine(text); ``` **你的作答** "C#入门" C:\Users\Kister 第一行 第二行 \"把"保留在字符串里了 \\把\保留在字符串里了 \n是换行 **批改区** 判定:正确。 点评:输出结果和三个转义字符的作用都写对了:`\"` 保留双引号,`\\` 保留反斜杠,`\n` 表示换行。 ### 4. [代码阅读题] 格式转换 阅读下面代码,并回答问题: ```csharp int a = 10; double b = a; int c = (int)12.8; string s = 123.ToString(); ``` 问题: 1. 哪一行发生了隐式转换? 2. 哪一行发生了显式转换? 3. 哪一行把其他类型转成了 `string`? 4. 为什么 `Parse` 方法通常更适合“字符串转数字”这类场景? **你的作答** 1.第2行 2.第3、4行 3.第四行 4.因为转不了数字对象会报错 **批改区** 判定:部分正确。 点评: - 第 1 问答对了,隐式转换是第 2 行:`double b = a;` - 第 2 问应主要写第 3 行:`int c = (int)12.8;` - 第 4 行 `123.ToString()` 更适合归类为“转成字符串”,不算这里重点考的强制类型转换 - 第 4 问你的回答有点偏了。更准确地说:`Parse` 本来就是专门把字符串内容解析成目标数字类型的方法,所以特别常用于“字符串转数字” ### 5. [代码阅读题] 字符串拼接和三目运算符 写出下面变量的最终值,并说明原因。 ```csharp string str1 = "" + 1 + 2 + 3; string str2 = 1 + 2 + 3 + ""; string result = 5 > 3 ? "真" : "假"; ``` **你的作答** str1为"123",""让string知道这是在字符串拼接,123会作为字符串拼起来 str2为"6",""在最后,所以前面做的是运算而非拼接 result为"真" **批改区** 判定:正确。 点评:`str1` 是 `"123"`,`str2` 是 `"6"`,`result` 是 `"真"`,你的原因分析也对,说明你已经能区分“先做数值运算”还是“先做字符串拼接”了。 --- ## 基础 ### 6. [判断题] 值类型和引用类型 请判断下面说法是对还是错,如果错了请改正: 1. `int` 是引用类型 2. `string` 是引用类型 3. 数组是值类型 4. 值类型赋值时通常复制的是数据本身 5. 引用类型变量保存的是对象的引用 **你的作答** 1.错,int是典型值类型 2.对 3.错,数组存放的是值,本身是引用类型 4.对 5.对 **批改区** 判定:基本正确。 点评:整体判断对了。第 3 小题建议表述成“数组本身是引用类型,数组元素可以是值类型也可以是引用类型”,这样会更严谨,不要把“数组存放的是值”说得太绝对。 ### 7. [代码阅读题] 传值和传引用 阅读下面代码,写出最终输出结果,并解释原因。 ```csharp static void ChangeA(int x) { x = 100; } static void ChangeB(int[] arr) { arr[0] = 100; } int a = 1; int[] b = { 1 }; ChangeA(a); ChangeB(b); Console.WriteLine(a); Console.WriteLine(b[0]); ``` **你的作答** 1 100 a作为值类型传递值本身而不是地址,函数内部更改的是函数内部x地址的值,所以外部还是1 数组b作为引用类型传递的是地址,函数内部修改的是b引用地址上的值,所以外部也改变了 **批改区** 判定:正确。 点评:输出 `1` 和 `100` 都对,核心原因也说对了。更严谨一点可以记成: - `int` 传入方法时改的是副本 - 数组传入方法时传的是引用的副本,但这个副本仍然指向同一个数组对象,所以能改到内部元素 ### 8. [简答题] ref 和 out 根据你的笔记,总结 `ref` 和 `out` 的区别,至少写 3 点。然后再补充一个你认为适合用 `out` 的使用场景。 **你的作答** 1.ref在使用时必须在外部赋值,out在传入前不能赋值,或赋值和传出值无关 2.相反out在函数内部必须赋值,ref可以不 3.out流向只输出,ref是双向 out的应用场景可以是TryParse,将某个类型转换为int类型而int默认情况下(不加?修饰)不能为null,所以必须用out在TryParse方法体内返回一个值出来 **批改区** 判定:部分正确。 点评: - `ref` 传入前必须先初始化,这点你答对了 - `out` 传入前是“可以不初始化”,不是“不能初始化” - `out` 在方法结束前必须赋值,这点你答对了 - 把 `out` 理解成“额外带回一个结果”也对,`TryParse` 的例子很合适 补充:这里的关键不是“`int` 不能为 `null`”,而是方法除了返回“是否成功”之外,还要再返回一个解析出来的值,所以适合用 `out` ### 9. [找错题] out 参数 下面代码能否通过编译?如果不能,请指出原因。 ```csharp void Test(ref int a, out int b) { a++; } int x = 1; int y; Test(ref x, out y); ``` **你的作答** 不能,out传递的参数在函数结束时没有赋值 **批改区** 判定:正确。 点评:问题抓得很准,`out int b` 在方法结束前没有赋值,所以代码不能通过编译。 ### 10. [应用题] 变长参数 请根据你的笔记,回答下面两个问题: 1. `params` 变长参数有哪些规则? 2. 请写出一个方法签名,使它能够接收任意个 `int` 参数,并返回它们的和。只写方法签名也可以,愿意的话也可以顺手把方法体写出来。 **你的作答** 1.只能作用于数组对象,必须放在方法参数列表的末尾 2.访问修饰符 static int Sum(params int[] arr){} 具体里面是一个循环了arr.Length次的数组,逐次取出并累加到一个变量里并最终将他返回 **批改区** 判定:部分正确。 点评:你答到了两条关键规则: - `params` 后面必须是数组 - `params` 参数必须放在参数列表末尾 还差一条: - 一个方法里最多只能有一个 `params` 参数 你写的方法签名 `static int Sum(params int[] arr)` 是正确的。 ### 11. [代码阅读题] 枚举 阅读下面代码并回答问题: ```csharp enum E_PlayerState { Idle, Move, Attack = 10, Die } ``` 问题: 1. `Idle`、`Move`、`Attack`、`Die` 对应的整数值分别是多少? 2. 为什么笔记里说枚举和 `switch` “天生一对”? 3. 为什么一般不推荐把枚举量写成中文? **你的作答** 1.0、1、10、11 2.因为switch的case可以识别传递的枚举类型,或直接用枚举的"下标"值作为对象 3.中文在编译时容易报错,兼容性不好 **批改区** 判定:基本正确。 点评: - 第 1 问数值全对:`0、1、10、11` - 第 2 问意思基本对,可以再补一句:用枚举配合 `switch` 时,`case` 可以直接按状态名写,代码比直接写数字更清晰 - 第 3 问建议改成“命名规范、可读性和兼容性更好”,而不是说“中文一定会编译报错” --- ## 核心 ### 12. [简答题] 继承和 base 根据你的笔记,回答下面问题: 1. `Dog : Animal` 表达的是什么关系? 2. 为什么子类里通常可以访问父类的 `protected` 成员,但不能直接访问 `private` 成员? 3. `base(name)` 的作用是什么? 4. 为什么说“构造函数不会被继承”? **你的作答** 1.Dog继承Animal的关系 2.因为private修饰的成员只有当前类自己才能访问,而protect修饰的成员是当前或继承该类的子类都可以访问 3.base(name)意思是继承的父类的name成员 4.因为首先违反了命名规则,如果子类继承了父类的构造函数,那么函数名和子类对不上,其次重名之后程序会不知道调用的是子类的构造函数还是父类的构造函数 **批改区** 判定:部分正确。 点评: - 第 1 问和第 2 问基本对 - ==第 3 问答错了,`base(name)` 的作用是“调用父类构造函数,并把 `name` 传给父类==” - 第 4 问的更准确说法是:构造函数属于各自类型,不会被子类直接继承,子类只是可以通过 `base(...)` 去调用父类构造函数 ==补一句你这题最该记住的话:`base(name)` 不是“继承父类成员”,而是“调用父类构造函数”。== ### 13. [代码阅读题] 静态成员 阅读下面代码,写出输出结果,并解释为什么这里更适合用 `Player.Count` 来访问。 ```csharp class Player { public static int Count; public string Name; public Player(string name) { Name = name; Count++; } } Player p1 = new Player("A"); Player p2 = new Player("B"); Console.WriteLine(Player.Count); ``` 然后再补充回答: 1. 静态成员和实例成员最核心的区别是什么? 2. `const` 和 `static readonly` 的区别是什么? **你的作答** 因为static int了count成员后,所有player类实例化对象在操作count时只会操作同一个对象,这样可以统一一个值,如果不是static的话每个player实例化对象都会操作自己的count,反而对不上总人数了。 1.静态成员首先在程序调用到时会自动创建内存并一直存在直到程序结束不会被GC清理,实例成员如果没有引用后可以被GC清理,静态成员只有唯一一个地址/标识,但实例成员可以是被实例化成很多地址/成员 2.const和static readonly的区别是const必须在声明时赋值,static readonly可以先声明,后续在构造函数中赋值 **批改区** 判定:部分正确。 点评: - 你对“为什么适合用 `Player.Count`”的理解是对的:它是所有对象共享的一份数据 - 但这题少写了最直接的一问:输出结果是 `2` - “静态成员一直存在直到程序结束不会被 GC 清理”这个说法太绝对了,先不要这么记 更稳的记法: - 静态成员属于类型本身 - 实例成员属于具体对象 - `const` 必须声明时赋值,属于编译期常量 - `static readonly` 可以在声明时赋值,也可以在静态构造函数中赋值,属于运行期确定 ### 14. [代码编写题] 成员属性 请补全下面这个类中的 `Age` 属性,要求: 1. 外部可以读和写 2. 当赋值小于 0 时,内部实际保存为 0 ```csharp class Person { private int age; // 请在这里补全 Age 属性 } ``` **你的作答** ```csharp class Person { private int age; public int Age { get { return age; } set { if(value < 0) age = 0; age = value; } } } ``` **批改区** 判定:有错误,需要修改。 点评:你的 `get` 写对了,但 `set` 里有逻辑漏洞: ```csharp if(value < 0) age = 0; age = value; ``` 这样即使 `value < 0`,下一行还是会把负数重新赋给 `age`,所以最终并不会变成 `0`。 更正后可以写成: ```csharp public int Age { get { return age; } set { if (value < 0) { age = 0; return; } age = value; } } ``` ### 15. [代码编写题] 索引器 请根据你的笔记,给下面这个类补一个索引器,让外部可以通过 `team[0]` 这种方式读写成员名。 ```csharp class Team { private string[] members = new string[3]; // 请在这里补全索引器 } ``` 补充回答: 1. 为什么索引器的名字固定写成 `this`? 2. 为什么说它“很像属性,但访问器带参数”? **你的作答** ```csharp class Team { private string[] members = new string[3]; public string this[int index]{get{return members[index];}} } ``` 1.表示该类本身 2.因为它可以get或set,但访问需要下标 **批改区** 判定:部分正确。 点评:你已经把索引器最核心的语法写出来了,也知道它为什么像属性。但题目要求“可以读写成员名”,所以这里只写 `get` 还不够,还需要补 `set`。 参考写法: ```csharp public string this[int index] { get { return members[index]; } set { members[index] = value; } } ``` ### 16. [综合设计题] 小型角色类 请你自己设计一个简单的 `Player` 或 `Student` 类,要求至少用到下面 4 个知识点中的 3 个: - 枚举 - 继承 - 静态成员 - 属性 - 索引器 要求: 1. 写出核心代码 2. 简单说明你每一部分分别用到了什么知识点 3. 如果你不想写完整代码,也可以写类结构和关键成员 **你的作答** ```Csharp using System; namespace Exercise2 { /// /// 学生年级枚举。 /// enum StudentLevel { Freshman, Sophomore, Junior, Senior } /// /// 表示三门课程成绩的值类型。 /// struct Score { public int Chinese; public int Math; public int English; /// /// 三门成绩总分。 /// public int Total { get { return Chinese + Math + English; } } /// /// 初始化成绩。 /// public Score(int chinese, int math, int english) { Chinese = chinese; Math = math; English = english; } /// /// 重载 + 运算符,用于合并两个成绩对象。 /// public static Score operator +(Score left, Score right) { return new Score( left.Chinese + right.Chinese, left.Math + right.Math, left.English + right.English ); } } /// /// 学生基类。 /// class Student { public const int PassLine = 180; private int _id; private string _name; private int _age; private Score _score; /// /// 统计已创建的学生对象总数。 /// public static int StudentCount { get; private set; } public int Id { get { return _id; } } public StudentLevel Level { get; private set; } /// /// 学生姓名属性。 /// public string Name { get { return _name; } set { _name = string.IsNullOrWhiteSpace(value) ? "未命名" : value; } } /// /// 学生年龄属性,年龄不能小于 0。 /// public int Age { get { return _age; } set { if (value < 0) { throw new ArgumentException("年龄不能小于 0。"); } _age = value; } } /// /// 学生成绩属性,三门成绩都不能小于 0。 /// public Score Score { get { return _score; } set { if (value.Chinese < 0 || value.Math < 0 || value.English < 0) { throw new ArgumentException("成绩不能小于 0。"); } _score = value; } } /// /// 初始化学生对象。 /// public Student(int id, string name, int age, Score score, StudentLevel level) { _id = id; Name = name; Age = age; Score = score; Level = level; StudentCount++; } /// /// 输出学生信息。 /// public virtual void PrintInfo() { Console.WriteLine("============================================"); Console.WriteLine($"学号:{Id}"); Console.WriteLine($"姓名:{Name}"); Console.WriteLine($"年龄:{Age}"); Console.WriteLine($"年级:{Level}"); Console.WriteLine($"语文:{Score.Chinese}"); Console.WriteLine($"数学:{Score.Math}"); Console.WriteLine($"英语:{Score.English}"); Console.WriteLine($"总分:{Score.Total}"); Console.WriteLine(Score.Total >= PassLine ? "结果:及格" : "结果:不及格"); } } /// /// 研究生类,继承自 Student。 /// class GraduateStudent : Student { public string ResearchDirection { get; set; } /// /// 初始化研究生对象,并调用父类构造函数。 /// public GraduateStudent( int id, string name, int age, Score score, StudentLevel level, string researchDirection) : base(id, name, age, score, level) { ResearchDirection = string.IsNullOrWhiteSpace(researchDirection) ? "未定方向" : researchDirection; } /// /// 输出研究生信息。 /// public override void PrintInfo() { base.PrintInfo(); Console.WriteLine($"研究方向:{ResearchDirection}"); } } /// /// 班级管理类。 /// class ClassRoom { private const int MaxStudentCount = 40; private readonly Student[] _students = new Student[MaxStudentCount]; private int _count = 0; /// /// 当前班级已添加学生数量。 /// public int Count { get { return _count; } } /// /// 索引器:通过下标访问学生对象。 /// public Student this[int index] { get { if (index < 0 || index >= _count) { throw new IndexOutOfRangeException("索引超出范围。"); } return _students[index]; } } /// /// 一次添加多个学生。 /// public void AddStudents(params Student[] students) { for (int i = 0; i < students.Length; i++) { if (_count >= MaxStudentCount) { Console.WriteLine("班级已满,无法继续添加学生。"); return; } _students[_count] = students[i]; _count++; } } /// /// 按学号查找学生。 /// public bool FindStudent(int id, out Student result) { for (int i = 0; i < _count; i++) { if (_students[i].Id == id) { result = _students[i]; return true; } } result = null!; return false; } /// /// 给某门成绩加分,演示 ref 的使用。 /// public static void AddBonus(ref int score, int bonus) { score += bonus; } } /// /// 程序入口。 /// class Program { static void Main(string[] args) { Student s1 = new Student( 1, "小周", 18, new Score(90, 95, 85), StudentLevel.Freshman ); Student s2 = new Student( 2, "小王", 19, new Score(80, 85, 90), StudentLevel.Sophomore ); GraduateStudent g1 = new GraduateStudent( 3, "小李", 24, new Score(88, 90, 92), StudentLevel.Senior, "人工智能" ); ClassRoom classRoom = new ClassRoom(); classRoom.AddStudents(s1, s2, g1); Console.WriteLine("索引器访问第 2 个学生:"); Console.WriteLine(classRoom[1].Name); Console.WriteLine(); Console.WriteLine("按学号查找学生:"); if (classRoom.FindStudent(2, out Student result)) { result.PrintInfo(); } else { Console.WriteLine("没找到该学生。"); } Console.WriteLine(); Console.WriteLine("给小周的语文成绩加 10 分:"); Score updatedScore = s1.Score; ClassRoom.AddBonus(ref updatedScore.Chinese, 10); s1.Score = updatedScore; Console.WriteLine($"加分后语文:{s1.Score.Chinese}"); Console.WriteLine($"加分后总分:{s1.Score.Total}"); Console.WriteLine(); Console.WriteLine($"当前已创建的学生对象总数:{Student.StudentCount}"); Console.WriteLine(); Score totalScore = s1.Score + s2.Score; Console.WriteLine("s1 + s2 的成绩合并结果:"); Console.WriteLine($"语文合计:{totalScore.Chinese}"); Console.WriteLine($"数学合计:{totalScore.Math}"); Console.WriteLine($"英语合计:{totalScore.English}"); Console.WriteLine($"总分合计:{totalScore.Total}"); Console.WriteLine(); Console.WriteLine("普通学生信息:"); s1.PrintInfo(); Console.WriteLine(); Console.WriteLine("研究生信息:"); g1.PrintInfo(); Console.WriteLine(); Console.WriteLine("说明:"); Console.WriteLine("Score 适合用 struct,因为它只是一个小型成绩数据组合,拷贝成本低,语义上更像值。"); Console.WriteLine("Student 适合用 class,因为学生包含身份信息,通常需要被多个地方共享和持续修改。"); } } } ``` **批改区** 判定:优秀。 点评:这题完成度很高,已经明显超出“只要能覆盖 3 个知识点”的要求了。你实际用到了: - 枚举 - 继承 - 静态成员 - 属性 - 索引器 - 甚至还额外用了 `struct`、运算符重载、`params`、`ref`、`out` 优点: - 类之间职责比较清楚 - `Student` / `GraduateStudent` 的继承关系自然 - `StudentCount`、属性封装、索引器都写得比较完整 - 能把多个知识点串起来,说明你不是只会背概念,而是在尝试组织代码结构 小建议: - 如果你想让 `ClassRoom` 的索引器更完整,也可以再补一个 `set` - `null!`、运算符重载这些已经超出当前这套题的核心范围了,不过写出来说明你有主动扩展 这道题我给高评价,思路和完成度都很好。