# CSharp 综合练习 02 ## 说明 - 本题单基于你当前已经完成的 C# 笔记整理。 - 本次重点覆盖: - 上一套练习里的易错点回收 - `多态` - `抽象类和抽象方法` - `接口` - `密封类` - `密封函数` - `命名空间` - `抽象类和接口的区别` - 你可以直接在每道题下方的 `你的作答` 区域填写答案。 - 我后续会继续在 `批改区` 直接给你批改和点评。 --- ## 第一部分:错题回收 ### 1. [分类题] 格式转换再辨析 请把下面每一行代码分别归类为: - 隐式转换 - 显式强制转换 - 转成字符串 - 以上都不是 ```csharp int a = 10; double b = a; int c = (int)12.8; string s = 123.ToString(); bool ok = int.TryParse("123", out int value); ``` 要求: 1. 给第 2~5 行分别分类 2. 用一句话说明 `Parse` / `TryParse` 为什么常用于“字符串转数字” **你的作答** ``` 2.隐式转换 3.显式强制转换 4.转成字符串 5.以上都不是 `Parse` 本来就是专门把字符串内容解析成目标数字类型的方法,所以特别常用于“字符串转数字” ``` **批改区** 判定:正确。 点评:第 2~5 行的分类都对,`Parse / TryParse` 的用途说明也准确。你这题已经把“强转”“转字符串”“解析字符串”区分开了。 ### 2. [简答题] ref 和 out 精准区分 请至少写出 `ref` 和 `out` 的 3 个区别,并额外回答下面两个判断: 1. `out` 参数在传入前能不能已经有值? 2. `TryParse` 这种写法为什么更适合用 `out` 而不是 `ref`? **你的作答** ref和out的区别 1.ref必须在传入前赋值 2.out必须在传出前赋值 1.out传入前可以有值 2.因为out传出前必须赋值,更符合TryParse的应用情景 **批改区** 判定:部分正确。 点评: - 你答对了两点:`ref` 传入前必须先赋值,`out` 在方法结束前必须赋值 - `out` 传入前“可以有值”,这点你也答对了 - 但题目要求至少写 3 个区别,你这里只写出了 2 个核心点 建议补上这一句: - `ref` 更像“把已有变量传进去继续读写” - `out` 更像“额外带回一个结果” `TryParse` 适合 `out` 的原因你答到了主线:它除了返回“是否成功”,还要把解析结果再带出来。 ### 3. [应用题] params 规则回收 根据你的笔记,回答下面问题: 1. `params` 参数有哪些规则? 2. 下面两个方法签名,哪个是合法的?为什么? ```csharp void Test1(params int[] arr, int x) void Test2(int x, params int[] arr) ``` 3. 一个方法里能不能出现两个 `params` 参数? **你的作答** 1.params的规则有1.只能修饰数组类型对象,2.必须放在函数的参数列表末尾,3.一个函数只能有一格变长参数 2.第二个是合法的,因为第一个没有把变长数组放在参数列表的末尾 3.不能,这样的写法是不合法的 **批改区** 判定:正确。 点评:三条规则都答对了: - `params` 后面必须是数组 - 必须放在参数列表末尾 - 一个方法里最多只能有一个 `params` `Test2` 合法、`Test1` 不合法,这里判断没有问题。 ### 4. [代码阅读题] base 的真正作用 阅读下面代码并回答问题: ```csharp class Animal { public string Name; public Animal(string name) { Name = name; Console.WriteLine("Animal 构造"); } } class Dog : Animal { public Dog(string name) : base(name) { Console.WriteLine("Dog 构造"); } } Dog d = new Dog("旺财"); Console.WriteLine(d.Name); ``` 问题: 1. `base(name)` 的作用是什么? 2. 代码执行时,哪一句先输出? 3. 为什么说这里不是“继承了父类的构造函数”? **你的作答** 1.base(name)是复用父对象的构造函数来给name对象初始化 2.先输出父对象的cw 3.没有为什么,就不是继承,而是调用父类构造函数 **批改区** 判定:部分正确。 点评: - 你知道 `base(name)` 和父类构造函数有关,这个方向是对的 - 但更准确的说法是:`base(name)` 是“调用父类构造函数,并把 `name` 传过去” - 这里先输出的是 `Animal 构造` 第 3 问你结论对,但解释太短了。更完整的记法是: - 构造函数不会被继承 - 子类只是可以在自己的构造函数里用 `base(...)` 调用父类构造函数 ### 5. [代码阅读题] 静态成员本质 阅读下面代码,写出输出结果,并回答问题: ```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. 为什么这里更适合通过 `Player.Count` 访问,而不是让每个对象自己保存一份人数? 3. 静态成员和实例成员最核心的区别是什么? **你的作答** 1.2 2.自己保管自己的容易出现数量不一样的问题,都通过一个对象管理更统一 3.静态成员有唯一性,实例成员可以有很多 **批改区** 判定:正确。 点评:输出 `2`,原因分析也对。这里最核心的一句可以记成: - 静态成员属于类本身,所有对象共享 - 实例成员属于具体对象,每个对象各有一份 ### 6. [改错题] 属性的 set 逻辑 下面这个属性写法有逻辑错误。请指出错误,并改成正确版本。 ```csharp public int Age { get { return age; } set { if (value < 0) { age = 0; } age = value; } } ``` **你的作答** set最终会被赋值为value,if的逻辑被忽略了 改正版本就是往里加个return **批改区** 判定:部分正确。 点评:你已经抓到错误本质了,问题确实在于最后还是执行了 `age = value;`,把前面的判断覆盖掉了。 不过这题要求“改成正确版本”,所以最好把代码完整写出来,例如: ```csharp public int Age { get { return age; } set { if (value < 0) { age = 0; return; } age = value; } } ``` ### 7. [代码编写题] 索引器补全 请补全下面类中的索引器,要求外部既可以读,也可以写: ```csharp class Team { private string[] members = new string[3]; // 请补全索引器 } ``` 补充回答: 1. 为什么索引器名字固定写 `this`? 2. 为什么它“很像属性,但访问器带参数”? **你的作答** ```Csharp public string this[int index] { get { return members[index]; } set { memebers[index] = value; } } ``` 1.因为规定的语法就是这么写的,别再问这么傻逼的问题了,或者说代表自己这个类 2.因为他要给别人用所以必须带参数不带参数怎么知道要访问的是哪个 **批改区** 判定:部分正确。 点评: - 结构写对了,`get / set` 也都补上了 - 但代码里有一个拼写错误:`memebers` 应该是 `members`,不然会编译报错 补充回答里,比较稳的表述可以记成: - 索引器名字固定写 `this`,这是语法规定,表示当前对象支持 `[]` 访问 - 它像属性,是因为也有 `get / set` - 它又和普通属性不同,因为访问时要带索引参数,比如 `team[0]` --- ## 第二部分:核心专题 ### 8. [简答题] 多态的核心句子 根据你的笔记,用自己的话解释下面几件事: 1. 什么是多态? 2. `virtual`、`override`、`base` 分别起什么作用? 3. “编译时看变量类型,运行时看实际对象” 这句话是什么意思? **你的作答** 1.父对象在子对象中,同样的行为有不同的表现 2.virtual表示这是个虚方法,可以在子类中被重写,override表示这是个重写的方法,base表示访问父类成员 3.如果我有两个类Person和Player是继承关系,都有SayHi方法,如果我声明时用父类,装父类,用父类方法出来的是父类方法,子类也是同理,但是父类装子类时,再用SayHi就会用父类方法而不是具体的子类方法,重写能规避这个问题 **批改区** 判定:部分正确。 点评: - 你知道多态是在“同样调用下表现不同”,方向是对的 - `virtual / override / base` 的作用也基本对 但第 3 问你那句“父类装子类时,再用 SayHi 就会用父类方法”是错的。 如果父类方法是 `virtual`,子类正确 `override` 了,那么父类引用指向子类对象时,调用的是子类重写后的版本。 这题最稳的记法是: - 编译时看变量类型:决定你“能调用哪些成员” - 运行时看实际对象:决定你“真正执行哪个重写版本” ### 9. [代码阅读题] override 和 new 的区别 阅读下面两段代码,分别写出输出结果,并说明为什么不同。 第一段: ```csharp class Father { public virtual void Say() { Console.WriteLine("Father"); } } class Son : Father { public override void Say() { Console.WriteLine("Son"); } } Father f = new Son(); f.Say(); ``` 第二段: ```csharp class Father { public void Say() { Console.WriteLine("Father"); } } class Son : Father { public new void Say() { Console.WriteLine("Son"); } } Father f = new Son(); f.Say(); ``` **你的作答** 1.Son因为重写了 2.Father因为没重写,且容器是父类所以还是在用父类方法 **批改区** 判定:正确。 点评:第一段输出 `Son`,第二段输出 `Father`,原因你也说到了: - `override` 是真正的多态重写 - `new` 只是隐藏父类同名成员,不是多态重写 ### 10. [判断题] 密封类和密封函数 请判断下面说法是对还是错,如果错了请改正: 1. `sealed class` 表示这个类不能再被继承 2. `sealed override` 会让当前类也不能再被继承 3. 普通方法可以直接写成 `sealed void Test()` 4. 密封类不能创建对象 5. 密封类和抽象类不能同时使用 **你的作答** 1.T 2.F,只是方法不能被重写了,不会不让类被继续继承 3.F,都没重写,不能直接加sealed 4.F,可以创建对象,只是不能再被继承了 5.T **批改区** 判定:正确。 点评:五个判断都对。你已经把: - `sealed class` - `sealed override` - 普通方法不能直接 `sealed` - 密封类仍可创建对象 - `sealed` 和 `abstract` 不能同时用 这几个点区分清楚了。 ### 11. [代码阅读题] sealed override 阅读下面代码,判断哪些地方合法,哪些地方不合法,并说明原因。 ```csharp class Animal { public virtual void Speak() { Console.WriteLine("动物叫"); } } class Dog : Animal { public sealed override void Speak() { Console.WriteLine("狗叫"); } } class Husky : Dog { public override void Speak() { Console.WriteLine("哈士奇叫"); } } ``` 补充回答: 1. `sealed override` 和 `sealed class` 的区别是什么? 2. `Dog` 类本身还能不能被继承? **你的作答** Husky类中父类已经把Speak Sealed了,不能继续重写 1.前者只是保证该方法不会被继续重写,后者是保证整个类不被继续继承 2.可以,类本身没被Sealed **批改区** 判定:正确。 点评:你抓住了关键点:`Husky` 里那段 `override Speak()` 不合法,因为 `Dog.Speak()` 已经被 `sealed override` 封住了。 补充两问你也答对了: - `sealed override` 只封成员 - `sealed class` 封整个类 - `Dog` 本身还能继续被继承 ### 12. [简答题] 抽象类和抽象方法 根据你的笔记,回答下面问题: 1. 抽象类为什么不能直接 `new`? 2. 抽象方法和普通方法最核心的区别是什么? 3. 抽象类里能不能有构造函数、普通方法、字段、属性? 4. 普通子类继承抽象类后,必须做什么? **你的作答** 1.因为规则就是这样的,abstract类可以被理解为模板,是抽象的,不是具体的 2.普通类是具体的可以直接实例化对象,抽象类必须先通过重写才能有实例 3.可以有,只要子类去实现具体实例就可以了 4.必须重写抽象类中声明的构造函数、方法、字段等 **批改区** 判定:部分正确。 点评: - 第 3 问答对了,抽象类里可以有构造函数、普通方法、字段、属性 - 第 1 问你的理解接近了,但更准确地说:抽象类是不完整类型,可能还包含没实现的抽象成员,所以不能直接 `new` - 第 2 问答偏了。题目问的是“抽象方法和普通方法”,不是“抽象类和普通类” - 第 4 问答错了。普通子类必须实现的是“所有抽象成员”,不是“重写构造函数、字段等” 建议你把这题压缩记成: - 抽象方法没有方法体 - 普通方法有方法体 - 普通子类继承抽象类后,必须 `override` 所有抽象成员 ### 13. [代码编写题] 抽象类 请根据你的笔记,补全下面代码: 要求: 1. `Animal` 是抽象类 2. `Speak()` 是抽象方法 3. `Dog` 继承 `Animal` 4. `Dog` 正确实现 `Speak()` ```csharp // 请补全 ``` **你的作答** ```Csharp abstract class Animal { public abstract void Speak(); } class Dog : Animal { public override void Speak() { Console.WriteLine("Woof!"); } } ``` **批改区** 判定:正确。 点评:代码满足题目要求: - `Animal` 是抽象类 - `Speak()` 是抽象方法 - `Dog` 继承 `Animal` - `Dog` 正确 `override` 了 `Speak()` 这题写得干净,没问题。 ### 14. [简答题] 接口 根据你的笔记,回答下面问题: 1. 接口主要表达的是什么? 2. 为什么接口更适合表示“能力”而不是“身份”? 3. 一个类能继承几个类?能实现几个接口? 4. 为什么实现接口成员时通常要写 `public`? **你的作答** 1.表达某个可以实现的功能,或者如你2中所说的能力 2.身份通常是唯一的,能力可以是多重的,如果把“能唱歌”当成身份(`Singer`作为抽象类),那么某个学生毕业成为程序员后可能不再是`Singer`,但实际他依然能唱歌。用接口则灵活保持这个能力。 3.一个类只能继承一个类,而接口没有数量限制 4.写成private外部无法调用,且违背原则编译错误 **批改区** 判定:正确。 点评:这题答得比较完整。 - 你对“接口表达能力而不是身份”的理解是到位的 - 第 3 问也对:类只能继承一个父类,但可以实现多个接口 - 第 4 问也对:实现接口成员时访问级别不能比接口契约更低 ### 15. [代码编写题] 接口实现 请完成下面代码,让 `Bird` 和 `Plane` 都能通过同一个方法被调用: 要求: 1. 定义一个 `IFlyable` 接口 2. `Bird` 和 `Plane` 都实现它 3. 写一个 `LetItFly(IFlyable flyable)` 方法 4. 在方法里调用飞行行为 ```csharp // 请补全 ``` ```Csharp using System.Security.Cryptography.X509Certificates; interface IFlyable { public void LetItFly() { Console.WriteLine("Flying..."); } } public class Bird : IFlyable { public Bird() { } public void LetItFly() { Console.WriteLine("Bird Flying... "); } } public class Plane : IFlyable { public Plane() { } public void LetItFly() { Console.WriteLine("Plane Flying... "); } } class Program { public static void LetItFly(IFlyable flyable) { flyable.LetItFly(); } static void Main(string[] args) { IFlyable f1 = new Bird(); IFlyable f2 = new Plane(); LetItFly(f1); LetItFly(f2); } } ``` **批改区** 判定:部分正确。 点评: - 你的代码确实达成了“Bird 和 Plane 能通过同一个方法统一调用”的目标 - `LetItFly(IFlyable flyable)` 这部分也写出来了 但按这套笔记的学习范围,更标准的写法应该是: - 接口里只声明方法,不写方法体 - 接口方法名通常写成 `Fly()`,外部统一调用方法再写成 `LetItFly(IFlyable flyable)` 也就是说,更推荐的结构是: ```csharp interface IFlyable { void Fly(); } ``` 然后在 `Bird` / `Plane` 里实现 `Fly()`。 你这题功能上基本成立,但接口的“契约”味道还可以再更清晰一点。 ### 16. [代码阅读题] 接口多实现 阅读下面代码,并回答问题: ```csharp interface IMovable { void Move(); } interface IAttackable { void Attack(); } class Player : IMovable, IAttackable { public void Move() { Console.WriteLine("玩家移动"); } public void Attack() { Console.WriteLine("玩家攻击"); } } ``` 问题: 1. 这段代码说明了接口的什么特点? 2. 如果 `Player` 只实现了 `Move()`,没实现 `Attack()`,会怎么样? 3. 为什么说接口可以弥补“类只能单继承”的限制? **你的作答** 1.面向能力编程 2.报错,继承了必须实现 3.因为接口让你给一个类“附加”多种不相关的行为能力,而这些行为无法(或不适合)放进单条继承链里。 **批改区** 判定:正确。 点评:三问都答对了。 - 这段代码体现了“一个类可以实现多个接口” - 少实现一个接口成员会直接报错 - 接口可以给一个类补充多种能力,这是它对单继承限制的重要补充 ### 17. [简答题] 命名空间 根据你的笔记,回答下面问题: 1. `namespace` 的作用是什么? 2. `using` 的作用是什么? 3. 如果 `Game` 和 `Tool` 两个命名空间里都有 `Player`,该怎么区分使用? 4. 命名空间和访问修饰符是不是一回事?为什么? **你的作答** 1.将程序划分为不同的区域,防止命名冲突 2.using的作用就和include一样,引用命名空间 3.Game.Player和Tool.Player 4.命名空间用于组织代码逻辑分组(解决命名冲突),访问修饰符用于控制成员的可访问性(封装);前者是“在哪”,后者是“谁能用”。 **批改区** 判定:正确。 点评:这题比较稳。 - `namespace` 负责组织代码、避免重名 - `using` 负责引入命名空间,简化书写 - 同名类型可以用完整限定名区分 - 命名空间和访问修饰符不是一回事 你最后那句“前者是在哪,后者是谁能用”总结得很好。 ### 18. [对比题] 抽象类和接口的区别 请根据你的笔记,从下面几个角度对比抽象类和接口: 1. 它们分别更适合表达什么? 2. 在“共同代码复用”方面,谁更适合? 3. 在“一个类拥有多种能力”方面,谁更适合? 4. 什么情况下更适合用抽象类,什么情况下更适合用接口? **你的作答** 1.一个是用来表达某个东西的通用模板,一个用来表达一个通用的方法 2.抽象类 3.接口 4.打个比方:鸟和飞机都能飞,但他们不属于一类东西,飞这个功能是都有的,所以用接口。人和猩猩都是猿,且都有猿类独有的特征如直立行走等所以可以用猿类抽象类 **批改区** 判定:基本正确。 点评: - 第 2、3、4 问都答到了重点 - 第 1 问再严谨一点可以写成: - 抽象类更适合表达“是什么” - 接口更适合表达“能做什么 / 具备什么能力” 你举的“鸟和飞机都会飞”这个例子是合适的,已经能说明接口为什么更适合表达能力。 ### 19. [综合设计题] 角色系统小练习 请你自己设计一个简单的角色系统,要求至少用到下面 6 个知识点中的 4 个: - 继承 - 多态 - 抽象类 - 接口 - 静态成员 - 属性 - 索引器 - 密封函数 - 命名空间 要求: 1. 写出核心代码 2. 标出你分别用到了哪些知识点 3. 简单说明为什么这里用抽象类,为什么这里用接口 4. 如果你愿意,也可以顺手加上一个你自己的小扩展 **你的作答** (待填写) **批改区** 判定:良好。 点评:我按你提供的 `D:\Visual Studio\Program\ReStudy CSharp\Restudy\Exercise3` 代码一起批改了。 这套代码里你实际用到了很多要求内的知识点: - 继承:`Player : Character`、`Enemy : Character`、`Car : Vehicle` - 多态:`Character[]` 中装 `Player` 和 `Enemy`,统一调用 `Move()` / `Attack()` - 抽象类:`Character`、`Vehicle` - 接口:`IDrivable` - 静态成员:`playerCount`、`enemyCount` - 属性:`maxPassenger`、`passengers`、`location` - 索引器:`Vehicle.this[int index]` - 密封函数:`Car` 中的 `sealed override Drive(...)` - 命名空间:`Program`、`Character`、`Vehicle`、`Vector` 优点: - 结构已经有“小项目”味道了,不只是单题代码 - 你能把抽象类、多态、接口、静态成员、索引器串起来用,这点很好 - `Character[]` 统一调度角色行为,这个设计是对路的 还可以继续改进的点: - 你在题目里没有把“为什么这里用抽象类、为什么这里用接口”写成文字说明,这一部分算缺失 - `IAttackable` 和 `IMovable` 文件目前没有真正接入 `Character / Player / Enemy` 体系,像是还没用上的草稿接口 - `IDrivable.Drive(Player player)` 这个接口写得稍微有点窄,如果以后不是只有 `Player` 能开车,这个接口会不够通用 综合看,这题已经明显达到要求,而且超出了“只用 4 个知识点”的下限。解释部分如果补得更完整,这题会更强。