CSharp(三十四)综合实战:学生管理系统(从零到完整项目)
目录
- 项目概述:我们要做什么
- 为什么需要综合练习
- 系统设计:类与类之间的关系
- 步骤1:定义枚举类型
- 步骤2:定义接口
- 步骤3:抽象基类 Person
- 步骤4:课程类 Course
- 步骤5:成绩类 Score
- 步骤6:学生类 Student(核心)
- 步骤7:教师类 Teacher
- 步骤8:班级类 ClassRoom
- 步骤9:报表工具类 ReportGenerator
- 步骤10:主程序 Main 串联所有
- 完整代码(可直接运行)
- 程序运行流程图
- 知识点速查表
- 扩展练习
- 小结
1. 项目概述:我们要做什么
一个真实场景
假设你是一个学校的教务系统开发人员。你需要写一个学生管理系统,功能包括:
教师 ──▶ 教授课程 ──▶ 给学生打成绩
│
▼
学生 ──▶ 查看自己的成绩单 ──▶ 班级排名
│
▼
班级报告生成
系统包含的角色
| 角色 | 能做什么 |
|---|---|
| 学生 | 自我介绍、学习、查看成绩单(平均分 + 每科成绩) |
| 教师 | 自我介绍、负责多门课程、给学生录入成绩 |
| 课程 | 有编号、名称、学分、授课教师 |
| 班级 | 包含多名学生、有班主任、排名、全班平均分 |
| 报表 | 自动生成班级报告(排名、平均分等) |
涉及的知识点
这个系统把前面学过的几乎所有 C# 类相关知识点串在一起:
- 枚举(Gender, Grade)
- 接口(IIdentifiable, IStudy)
- 抽象类(Person)
- 继承(Student : Person, Teacher : Person)
- 多态(virtual/override)
- 属性(自动属性、只读属性、表达式体属性)
- 静态成员(TotalStudents、ReportGenerator)
- 事件(ScoreAdded)
- 索引器(this[string courseCode])
- 构造函数与 base 关键字
- 集合(List<T>)
- LINQ(Average, FirstOrDefault, OrderByDescending)
2. 为什么需要综合练习
学习编程就像学做菜:
单个知识点 = 学会切菜、学会开火、学会放盐
综合练习 = 真正做出一道完整的菜
如果你只学了"切菜"就觉得自己会做菜,一到真正动手时就会发现——不知道先放油还是先放菜,不知道火候多大,不知道什么时候放盐。
这个综合案例就是让你把之前学的所有知识点串起来,做一道完整的"菜"。
3. 系统设计:类与类之间的关系
在写代码之前,先搞清楚谁和谁有关系:
IIdentifiable IStudy
▲ ▲
│ │
┌─────┴─────┐ │
│ Person │ │
│ (abstract)│ │
└─────┬─────┘ │
│ │
┌──────────┼──────────┐ │
▼ ▼ │
Teacher Student────────┘
│ │
│ 授课 │ 选修
▼ ▼
Course ◄──────────── Score(成绩)
│
│ 担任班主任
▼
ClassRoom ─── 包含 ──▶ Student(多个)
│
▼
ReportGenerator(静态工具类)
关系解读
| 关系 | 代码体现 | 解释 |
|---|---|---|
| 继承 | class Student : Person |
学生"是一种"人 |
| 实现接口 | class Student : IStudy |
学生"具备"学习能力 |
| 组合(一对多) | ClassRoom 里有 List<Student> |
班级包含多个学生 |
| 关联 | Teacher 里有 List<Course> |
教师教授多门课 |
| 依赖 | Score 里有 Course |
成绩依赖于某门课 |
4. 步骤1:定义枚举类型
枚举(enum)用于定义一组有名字的常量,让代码更有可读性:
// 性别枚举
public enum Gender
{
Male, // 男(值=0)
Female // 女(值=1)
}
// 年级枚举
public enum Grade
{
Freshman, // 大一(值=0)
Sophomore, // 大二(值=1)
Junior, // 大三(值=2)
Senior // 大四(值=3)
}
为什么用枚举而不是直接用 int?
// ❌ 不好:谁看得懂 "0" 是什么意思?
Student s = new Student("张三", 20, 0, "2024001", 1, "计算机科学");
// ↑男?↑大二? 不清晰!
// ✅ 好:一目了然
Student s = new Student("张三", 20, Gender.Male, "2024001", Grade.Sophomore, "计算机科学");
// ↑男 ↑大二
5. 步骤2:定义接口
接口定义"能做什么"的契约:
// 接口1:有身份标识
public interface IIdentifiable
{
string Id { get; } // 只读属性,所有实现者必须有 Id
}
// 接口2:学习能力
public interface IStudy
{
void Study(string subject, int hours); // 学习方法
double GetAverageScore(); // 获取平均分
}
接口的作用:当你看到 IStudy 接口,你就知道实现它的类一定能学习和能计算平均分。你不关心具体是谁,只关心有这个能力。
6. 步骤3:抽象基类 Person
Person 是所有"人"的模板,提取学生和教师的公共部分:
public abstract class Person : IIdentifiable // Person 实现了 IIdentifiable
{
// ===== 只读属性:创建后不能改 =====
public string Id { get; }
// ===== 可读写属性 =====
public string Name { get; set; }
public int Age { get; set; }
public Gender Gender { get; set; }
// ===== 构造函数:必填项 =====
public Person(string name, int age, Gender gender)
{
Id = Guid.NewGuid().ToString().Substring(0, 8); // 自动生成8位ID
Name = name;
Age = age;
Gender = gender;
}
// ===== 抽象方法:子类必须实现 =====
// 每个人都要会自我介绍,但方式不同
public abstract void Introduce();
// ===== 虚方法:子类可选择重写 =====
// 默认返回"人员",子类可以覆盖成"学生"或"教师"
public virtual string GetRole()
{
return "人员";
}
}
逐行讲解
| 代码 | 知识点 | 说明 |
|---|---|---|
public abstract class Person |
抽象类 | 不能 new Person(),只能被继承 |
: IIdentifiable |
接口实现 | Person 承诺提供 Id |
public string Id { get; } |
只读属性 | 只允许在构造函数/初始化器中赋值 |
Guid.NewGuid().ToString().Substring(0, 8) |
生成ID | 生成全球唯一标识,取前8位 |
public abstract void Introduce() |
抽象方法 | 没有方法体,子类必须重写 |
public virtual string GetRole() |
虚方法 | 有默认实现,子类可选是否重写 |
7. 步骤4:课程类 Course
课程类用于描述一门课程的基本信息:
public class Course
{
public string Code { get; } // 课程编号,如 "CS101"
public string Name { get; } // 课程名称,如 "C#程序设计"
public int Credits { get; } // 学分
public Teacher Teacher { get; set; } // 授课教师
// 构造函数:编号、名称、学分是必填项
public Course(string code, string name, int credits)
{
Code = code;
Name = name;
Credits = credits;
}
// 方便打印
public override string ToString() => $"[{Code}] {Name} ({Credits}学分)";
}
设计思路:编号/名称/学分是课程诞生就固定的(用只读属性),而授课教师是之后分配的(可读写属性)。
8. 步骤5:成绩类 Score
成绩类记录某学生某门课的分数:
public class Score
{
public Course Course { get; } // 哪门课
public double Value { get; } // 分数值
public DateTime Date { get; } // 录入时间
public Score(Course course, double value)
{
Course = course;
Value = value;
Date = DateTime.Now; // 自动记录录入时间
}
// 把数字分数转为 A/B/C/D/F 等级
public string GetGrade()
{
if (Value >= 90) return "A";
if (Value >= 80) return "B";
if (Value >= 70) return "C";
if (Value >= 60) return "D";
return "F";
}
public override string ToString() => $"{Course.Name}: {Value}分 ({GetGrade()})";
}
为什么 Score 是一个独立的类?
你可以把 Score 做成这样:
// ❌ 不好的设计:把成绩塞进学生类
Dictionary<Course, double> scores; // 存不了录入时间、等级等信息
但独立成一个类后,你可以随时扩展——比如增加"学期"、"是否补考"、"评语"等字段,而不用改数据结构。这就是面向对象"单一职责"原则的体现。
9. 步骤6:学生类 Student(核心)
这是整个系统最复杂的类,我们用分块讲解的方式逐个击破:
第一部分:类声明和属性
public class Student : Person, IStudy // 继承 Person,实现 IStudy
{
public string StudentNumber { get; } // 学号(只读)
public Grade Grade { get; set; } // 年级
public string Major { get; set; } // 专业
// 内部成绩列表(私有,外部通过方法访问)
private List<Score> Scores { get; } = new List<Score>();
// 静态字段:统计全校学生总数
private static int totalStudents = 0;
// 事件:有新成绩时触发通知
public event EventHandler<Score> ScoreAdded;
知识点对照:
| 代码 | 知识点 |
|---|---|
: Person, IStudy |
多继承:一个类 + 多个接口 |
= new List<Score>() |
属性初始化器(C# 6.0+) |
private static int totalStudents |
静态字段,属于类而非实例 |
event EventHandler<Score> |
事件(发布/订阅模式) |
第二部分:构造函数
public Student(string name, int age, Gender gender,
string studentNumber, Grade grade, string major)
: base(name, age, gender) // ← 先调用父类 Person 的构造函数
{
StudentNumber = studentNumber;
Grade = grade;
Major = major;
totalStudents++; // 每创建一个学生,总数+1
}
执行顺序:
1. base(name, age, gender) → Person 的构造函数
2. StudentNumber = ... → 自身字段赋值
3. totalStudents++ → 统计计数
第三部分:静态属性
// 静态属性:外部通过 Student.TotalStudents 访问
public static int TotalStudents => totalStudents;
第四部分:重写父类方法
// 重写抽象方法(必须)
public override void Introduce()
{
string genderStr = Gender == Gender.Male ? "男" : "女";
Console.WriteLine($"我叫{Name},{Age}岁,{genderStr}," +
$"学号{StudentNumber},{Major}专业大{(int)Grade + 1}学生");
}
// 重写虚方法(可选)
public override string GetRole() => "学生";
第五部分:实现接口方法
// 实现 IStudy 接口
public void Study(string subject, int hours)
{
Console.WriteLine($"{Name} 学习了 {subject} {hours} 小时");
}
public double GetAverageScore()
{
if (Scores.Count == 0) return 0;
return Scores.Average(s => s.Value); // LINQ 求平均
}
第六部分:核心业务方法
// 添加成绩(带验证)
public void AddScore(Course course, double value)
{
if (value < 0 || value > 100)
throw new ArgumentException("成绩必须在0-100之间");
Score score = new Score(course, value);
Scores.Add(score);
ScoreAdded?.Invoke(this, score); // 触发事件(通知订阅者)
}
ScoreAdded?.Invoke(this, score) 解读:
?.= 如果没人订阅事件,就不触发(避免空引用异常)this= 事件源(谁触发的)score= 事件数据(传递成绩信息)
第七部分:索引器
// 索引器:按课程代码查成绩
// 用法:student["CS101"] → 返回该课程分数
public double? this[string courseCode]
{
get
{
var score = Scores.FirstOrDefault(s => s.Course.Code == courseCode);
return score?.Value; // 找到返回分数,没找到返回 null
}
}
double?中的?表示可空类型,没找到时返回null。
第八部分:打印成绩单
public void PrintTranscript()
{
Console.WriteLine($"\n===== {Name} 的成绩单 =====");
Console.WriteLine($"学号:{StudentNumber} 专业:{Major}");
Console.WriteLine($"平均分:{GetAverageScore():F1}");
Console.WriteLine("科目成绩:");
foreach (var score in Scores)
{
Console.WriteLine($" {score}");
}
if (Scores.Count == 0)
{
Console.WriteLine(" 暂无成绩");
}
Console.WriteLine("=============================\n");
}
}
10. 步骤7:教师类 Teacher
教师类比学生类简单,但也继承自 Person:
public class Teacher : Person
{
public string Department { get; set; } // 所在院系
public string Title { get; set; } // 职称(教授/副教授/讲师)
private List<Course> courses = new List<Course>(); // 负责的课程
public Teacher(string name, int age, Gender gender,
string department, string title)
: base(name, age, gender)
{
Department = department;
Title = title;
}
// 重写抽象方法
public override void Introduce()
{
Console.WriteLine($"我是{Title}{Name},在{Department}系任教");
}
public override string GetRole() => "教师";
// 分配课程
public void AssignCourse(Course course)
{
courses.Add(course);
course.Teacher = this; // 双向关联:告诉课程"你被我教"
}
// 教师录入学生成绩(核心业务)
public void GradeStudent(Student student, Course course, double score)
{
Console.WriteLine($"{Name} 给 {student.Name} 在 {course.Name} 录入成绩:{score}");
student.AddScore(course, score);
}
}
11. 步骤8:班级类 ClassRoom
班级管理一组学生:
public class ClassRoom
{
public string Name { get; } // 班级名称
public Teacher HeadTeacher { get; set; } // 班主任
private List<Student> students = new List<Student>();
public int StudentCount => students.Count; // 表达式体属性
public ClassRoom(string name)
{
Name = name;
}
// ===== 索引器:按位置访问学生 =====
public Student this[int index] => students[index];
// ===== 添加学生 =====
public void AddStudent(Student student)
{
students.Add(student);
Console.WriteLine($"{student.Name} 加入班级 {Name}");
}
// ===== 按学号查找 =====
public Student FindByStudentNumber(string number)
{
return students.FirstOrDefault(s => s.StudentNumber == number);
}
// ===== 打印全班名单 =====
public void PrintAllStudents()
{
Console.WriteLine($"\n===== 班级 {Name} 学生名单(共{StudentCount}人)=====");
foreach (var s in students)
{
s.Introduce(); // ← 多态调用!
}
Console.WriteLine("============================================\n");
}
// ===== 班级平均分 =====
public double GetClassAverageScore()
{
if (students.Count == 0) return 0;
return students.Average(s => s.GetAverageScore());
}
// ===== 班级排名(按平均分降序)=====
public List<Student> GetRanking()
{
return students.OrderByDescending(s => s.GetAverageScore()).ToList();
}
}
亮点:PrintAllStudents() 中调用 s.Introduce() 时,虽然类型是 Student,但如果将来有 ExchangeStudent(交换生)继承 Student 并重写了 Introduce,这个循环不需要改任何代码就能正确调用。这就是多态的威力。
12. 步骤9:报表工具类 ReportGenerator
这是一个静态类,专注于生成班级报告:
public static class ReportGenerator
{
public static void GenerateClassReport(ClassRoom classroom)
{
Console.WriteLine(new string('=', 50));
Console.WriteLine($" {classroom.Name} 班级报告");
Console.WriteLine(new string('=', 50));
Console.WriteLine($"班主任:{classroom.HeadTeacher?.Name ?? "未指定"}");
Console.WriteLine($"学生人数:{classroom.StudentCount}");
Console.WriteLine($"班级平均分:{classroom.GetClassAverageScore():F1}");
Console.WriteLine("\n排名:");
var ranking = classroom.GetRanking();
for (int i = 0; i < ranking.Count; i++)
{
Console.WriteLine($" {i + 1}. {ranking[i].Name} - 平均分:{ranking[i].GetAverageScore():F1}");
}
Console.WriteLine(new string('=', 50));
}
}
为什么用静态类? 报表生成不需要保存状态,用的时候就调一下,不需要创建 new ReportGenerator()。
13. 步骤10:主程序 Main 串联所有
这一步就是把所有零件组装成完整系统:
13.1 创建课程
Course math = new Course("MATH101", "高等数学", 5);
Course english = new Course("ENG101", "大学英语", 4);
Course programming = new Course("CS101", "C#程序设计", 4);
13.2 创建教师并分配课程
Teacher teacher1 = new Teacher("王老师", 38, Gender.Male, "计算机科学", "教授");
Teacher teacher2 = new Teacher("陈老师", 42, Gender.Female, "数学", "副教授");
teacher1.Introduce(); // 输出:我是教授王老师,在计算机科学系任教
teacher2.Introduce(); // 输出:我是副教授陈老师,在数学系任教
// 一个教师可以教多门课
teacher1.AssignCourse(programming);
teacher2.AssignCourse(math);
teacher1.AssignCourse(english);
13.3 创建学生
Student s1 = new Student("张三", 20, Gender.Male, "2024001", Grade.Sophomore, "计算机科学");
Student s2 = new Student("李四", 21, Gender.Female, "2024002", Grade.Sophomore, "计算机科学");
Student s3 = new Student("王五", 19, Gender.Male, "2024003", Grade.Freshman, "软件工程");
Student s4 = new Student("赵六", 22, Gender.Female, "2024004", Grade.Junior, "计算机科学");
s1.Introduce(); // 输出:我叫张三,20岁,男,学号2024001,计算机科学专业大二学生
s2.Introduce();
13.4 教师录成绩
teacher1.GradeStudent(s1, programming, 92); // 王老师给张三录编程成绩92
teacher1.GradeStudent(s1, english, 85);
teacher2.GradeStudent(s1, math, 88);
teacher1.GradeStudent(s2, programming, 78);
teacher1.GradeStudent(s2, english, 90);
teacher2.GradeStudent(s2, math, 65);
teacher1.GradeStudent(s3, programming, 95);
teacher1.GradeStudent(s4, programming, 82);
teacher1.GradeStudent(s4, english, 79);
teacher2.GradeStudent(s4, math, 91);
13.5 组建班级
ClassRoom class1 = new ClassRoom("计科2024-1班");
class1.HeadTeacher = teacher1; // 王老师当班主任
class1.AddStudent(s1);
class1.AddStudent(s2);
class1.AddStudent(s3);
class1.AddStudent(s4);
13.6 打印成绩单
Console.WriteLine($"\n当前共有 {Student.TotalStudents} 名学生"); // 输出:4
s1.PrintTranscript();
s4.PrintTranscript();
13.7 使用索引器查成绩
Console.WriteLine($"s1 的编程成绩:{s1["CS101"]}"); // 输出:92
13.8 生成班级报告
class1.PrintAllStudents();
ReportGenerator.GenerateClassReport(class1);
13.9 多态演示
Console.WriteLine("\n===== 多态演示 =====");
List<Person> people = new List<Person> { s1, s2, teacher1, teacher2 };
foreach (Person p in people) // p 的类型是 Person
{
Console.Write($"身份:{p.GetRole()},");
p.Introduce(); // 但实际调用的是 Student 或 Teacher 的版本!
}
// 输出:
// 身份:学生,我叫张三,20岁,男,学号2024001,计算机科学专业大二学生
// 身份:学生,我叫李四,21岁,女,学号2024002,计算机科学专业大二学生
// 身份:教师,我是教授王老师,在计算机科学系任教
// 身份:教师,我是副教授陈老师,在数学系任教
这就是多态:用统一的
Person类型引用,但每个对象按照自己的方式执行Introduce()。
14. 完整代码(可直接运行)
using System;
using System.Collections.Generic;
using System.Linq;
// ========== 枚举 ==========
public enum Gender
{
Male, Female
}
public enum Grade
{
Freshman, Sophomore, Junior, Senior
}
// ========== 接口 ==========
public interface IIdentifiable
{
string Id { get; }
}
public interface IStudy
{
void Study(string subject, int hours);
double GetAverageScore();
}
// ========== 抽象基类 ==========
public abstract class Person : IIdentifiable
{
public string Id { get; }
public string Name { get; set; }
public int Age { get; set; }
public Gender Gender { get; set; }
public Person(string name, int age, Gender gender)
{
Id = Guid.NewGuid().ToString().Substring(0, 8);
Name = name;
Age = age;
Gender = gender;
}
public abstract void Introduce();
public virtual string GetRole()
{
return "人员";
}
}
// ========== 课程类 ==========
public class Course
{
public string Code { get; }
public string Name { get; }
public int Credits { get; }
public Teacher Teacher { get; set; }
public Course(string code, string name, int credits)
{
Code = code;
Name = name;
Credits = credits;
}
public override string ToString() => $"[{Code}] {Name} ({Credits}学分)";
}
// ========== 成绩记录 ==========
public class Score
{
public Course Course { get; }
public double Value { get; }
public DateTime Date { get; }
public Score(Course course, double value)
{
Course = course;
Value = value;
Date = DateTime.Now;
}
public string GetGrade()
{
if (Value >= 90) return "A";
if (Value >= 80) return "B";
if (Value >= 70) return "C";
if (Value >= 60) return "D";
return "F";
}
public override string ToString() => $"{Course.Name}: {Value}分 ({GetGrade()})";
}
// ========== 学生类 ==========
public class Student : Person, IStudy
{
public string StudentNumber { get; }
public Grade Grade { get; set; }
public string Major { get; set; }
private List<Score> Scores { get; } = new List<Score>();
private static int totalStudents = 0;
public event EventHandler<Score> ScoreAdded;
public Student(string name, int age, Gender gender,
string studentNumber, Grade grade, string major)
: base(name, age, gender)
{
StudentNumber = studentNumber;
Grade = grade;
Major = major;
totalStudents++;
}
public static int TotalStudents => totalStudents;
public override void Introduce()
{
string genderStr = Gender == Gender.Male ? "男" : "女";
Console.WriteLine($"我叫{Name},{Age}岁,{genderStr}," +
$"学号{StudentNumber},{Major}专业大{(int)Grade + 1}学生");
}
public override string GetRole() => "学生";
public void Study(string subject, int hours)
{
Console.WriteLine($"{Name} 学习了 {subject} {hours} 小时");
}
public void AddScore(Course course, double value)
{
if (value < 0 || value > 100)
throw new ArgumentException("成绩必须在0-100之间");
Score score = new Score(course, value);
Scores.Add(score);
ScoreAdded?.Invoke(this, score);
}
public double GetAverageScore()
{
if (Scores.Count == 0) return 0;
return Scores.Average(s => s.Value);
}
public double? this[string courseCode]
{
get
{
var score = Scores.FirstOrDefault(s => s.Course.Code == courseCode);
return score?.Value;
}
}
public void PrintTranscript()
{
Console.WriteLine($"\n===== {Name} 的成绩单 =====");
Console.WriteLine($"学号:{StudentNumber} 专业:{Major}");
Console.WriteLine($"平均分:{GetAverageScore():F1}");
Console.WriteLine("科目成绩:");
foreach (var score in Scores)
{
Console.WriteLine($" {score}");
}
if (Scores.Count == 0)
{
Console.WriteLine(" 暂无成绩");
}
Console.WriteLine("=============================\n");
}
}
// ========== 教师类 ==========
public class Teacher : Person
{
public string Department { get; set; }
public string Title { get; set; }
private List<Course> courses = new List<Course>();
public Teacher(string name, int age, Gender gender,
string department, string title)
: base(name, age, gender)
{
Department = department;
Title = title;
}
public override void Introduce()
{
Console.WriteLine($"我是{Title}{Name},在{Department}系任教");
}
public override string GetRole() => "教师";
public void AssignCourse(Course course)
{
courses.Add(course);
course.Teacher = this;
}
public void GradeStudent(Student student, Course course, double score)
{
Console.WriteLine($"{Name} 给 {student.Name} 在 {course.Name} 录入成绩:{score}");
student.AddScore(course, score);
}
}
// ========== 班级类 ==========
public class ClassRoom
{
public string Name { get; }
public Teacher HeadTeacher { get; set; }
private List<Student> students = new List<Student>();
public int StudentCount => students.Count;
public ClassRoom(string name)
{
Name = name;
}
public Student this[int index] => students[index];
public void AddStudent(Student student)
{
students.Add(student);
Console.WriteLine($"{student.Name} 加入班级 {Name}");
}
public Student FindByStudentNumber(string number)
{
return students.FirstOrDefault(s => s.StudentNumber == number);
}
public void PrintAllStudents()
{
Console.WriteLine($"\n===== 班级 {Name} 学生名单(共{StudentCount}人)=====");
foreach (var s in students)
{
s.Introduce();
}
Console.WriteLine("============================================\n");
}
public double GetClassAverageScore()
{
if (students.Count == 0) return 0;
return students.Average(s => s.GetAverageScore());
}
public List<Student> GetRanking()
{
return students.OrderByDescending(s => s.GetAverageScore()).ToList();
}
}
// ========== 静态工具类 ==========
public static class ReportGenerator
{
public static void GenerateClassReport(ClassRoom classroom)
{
Console.WriteLine(new string('=', 50));
Console.WriteLine($" {classroom.Name} 班级报告");
Console.WriteLine(new string('=', 50));
Console.WriteLine($"班主任:{classroom.HeadTeacher?.Name ?? "未指定"}");
Console.WriteLine($"学生人数:{classroom.StudentCount}");
Console.WriteLine($"班级平均分:{classroom.GetClassAverageScore():F1}");
Console.WriteLine("\n排名:");
var ranking = classroom.GetRanking();
for (int i = 0; i < ranking.Count; i++)
{
Console.WriteLine($" {i + 1}. {ranking[i].Name} - 平均分:{ranking[i].GetAverageScore():F1}");
}
Console.WriteLine(new string('=', 50));
}
}
// ========== 主程序 ==========
class Program
{
static void Main(string[] args)
{
// 1. 创建课程
Course math = new Course("MATH101", "高等数学", 5);
Course english = new Course("ENG101", "大学英语", 4);
Course programming = new Course("CS101", "C#程序设计", 4);
// 2. 创建教师
Teacher teacher1 = new Teacher("王老师", 38, Gender.Male, "计算机科学", "教授");
Teacher teacher2 = new Teacher("陈老师", 42, Gender.Female, "数学", "副教授");
teacher1.Introduce();
teacher2.Introduce();
// 分配课程给教师
teacher1.AssignCourse(programming);
teacher2.AssignCourse(math);
teacher1.AssignCourse(english);
// 3. 创建学生
Student s1 = new Student("张三", 20, Gender.Male, "2024001", Grade.Sophomore, "计算机科学");
Student s2 = new Student("李四", 21, Gender.Female, "2024002", Grade.Sophomore, "计算机科学");
Student s3 = new Student("王五", 19, Gender.Male, "2024003", Grade.Freshman, "软件工程");
Student s4 = new Student("赵六", 22, Gender.Female, "2024004", Grade.Junior, "计算机科学");
// 4. 学生自我介绍
s1.Introduce();
s2.Introduce();
// 5. 教师录成绩
teacher1.GradeStudent(s1, programming, 92);
teacher1.GradeStudent(s1, english, 85);
teacher2.GradeStudent(s1, math, 88);
teacher1.GradeStudent(s2, programming, 78);
teacher1.GradeStudent(s2, english, 90);
teacher2.GradeStudent(s2, math, 65);
teacher1.GradeStudent(s3, programming, 95);
teacher1.GradeStudent(s4, programming, 82);
teacher1.GradeStudent(s4, english, 79);
teacher2.GradeStudent(s4, math, 91);
// 6. 创建班级
ClassRoom class1 = new ClassRoom("计科2024-1班");
class1.HeadTeacher = teacher1;
class1.AddStudent(s1);
class1.AddStudent(s2);
class1.AddStudent(s3);
class1.AddStudent(s4);
// 7. 打印成绩单
Console.WriteLine($"\n当前共有 {Student.TotalStudents} 名学生");
s1.PrintTranscript();
s4.PrintTranscript();
// 8. 使用索引器查成绩
Console.WriteLine($"s1 的编程成绩:{s1["CS101"]}");
// 9. 班级报告
class1.PrintAllStudents();
ReportGenerator.GenerateClassReport(class1);
// 10. 多态演示
Console.WriteLine("\n===== 多态演示 =====");
List<Person> people = new List<Person> { s1, s2, teacher1, teacher2 };
foreach (Person p in people)
{
Console.Write($"身份:{p.GetRole()},");
p.Introduce();
}
}
}
运行输出预览
我是教授王老师,在计算机科学系任教
我是副教授陈老师,在数学系任教
我叫张三,20岁,男,学号2024001,计算机科学专业大二学生
我叫李四,21岁,女,学号2024002,计算机科学专业大二学生
王老师 给 张三 在 C#程序设计 录入成绩:92
王老师 给 张三 在 大学英语 录入成绩:85
陈老师 给 张三 在 高等数学 录入成绩:88
...
当前共有 4 名学生
===== 张三 的成绩单 =====
学号:2024001 专业:计算机科学
平均分:88.3
科目成绩:
C#程序设计: 92分 (A)
大学英语: 85分 (B)
高等数学: 88分 (B)
=============================
s1 的编程成绩:92
===== 班级 计科2024-1班 学生名单(共4人)=====
我叫张三,20岁,男,学号2024001,计算机科学专业大二学生
我叫李四,21岁,女,学号2024002,计算机科学专业大二学生
我叫王五,19岁,男,学号2024003,软件工程专业大一学生
我叫赵六,22岁,女,学号2024004,计算机科学专业大三学生
============================================
==================================================
计科2024-1班 班级报告
==================================================
班主任:王老师
学生人数:4
班级平均分:82.5
排名:
1. 张三 - 平均分:88.3
2. 赵六 - 平均分:84.0
3. 李四 - 平均分:77.7
4. 王五 - 平均分:95.0 ← 注意:王五只考了一门课
==================================================
===== 多态演示 =====
身份:学生,我叫张三,20岁,男,学号2024001,计算机科学专业大二学生
身份:学生,我叫李四,21岁,女,学号2024002,计算机科学专业大二学生
身份:教师,我是教授王老师,在计算机科学系任教
身份:教师,我是副教授陈老师,在数学系任教
15. 程序运行流程图
Main() 开始
│
├── 创建3门课程 (math, english, programming)
│
├── 创建2位教师 (teacher1, teacher2)
│ └── 分配课程给教师
│
├── 创建4名学生 (s1, s2, s3, s4)
│ └── totalStudents 自动变为 4
│
├── 录入成绩
│ ├── teacher1.GradeStudent(s1, programming, 92)
│ │ └── s1.AddScore(programming, 92)
│ │ └── 触发 ScoreAdded 事件
│ ├── ... (多条成绩录入)
│
├── 创建班级 (class1)
│ └── 添加4名学生,设班主任
│
├── 打印成绩单
│ └── 每科成绩 + 平均分 + 等级
│
├── 索引器查询 s1["CS101"] → 92
│
├── 班级报告
│ └── 排名 → s1第1, s2第2, ...
│
└── 多态演示
└── Person类型的List调用各自的Introduce()
16. 知识点速查表
| 知识点 | 在本项目中的位置 | 关键代码 |
|---|---|---|
| 枚举 | Gender、Grade | public enum Gender { Male, Female } |
| 接口 | IIdentifiable、IStudy | public interface IStudy { ... } |
| 抽象类 | Person | public abstract class Person |
| 抽象方法 | Person.Introduce() | public abstract void Introduce(); |
| 虚方法 | Person.GetRole() | public virtual string GetRole() |
| 继承 | Student : Person、Teacher : Person | class Student : Person, IStudy |
| 多态 | Main 的 foreach (Person p in people) | p.Introduce() 调用子类版本 |
| base 关键字 | Student 构造函数 | : base(name, age, gender) |
| 属性 | Person.Name、Course.Code | public string Name { get; set; } |
| 只读属性 | Person.Id、Course.Code | public string Id { get; } |
| 静态成员 | Student.totalStudents、ReportGenerator | private static int totalStudents |
| 事件 | Student.ScoreAdded | public event EventHandler<Score> ScoreAdded |
| 索引器 | Student.this[string]、ClassRoom.this[int] | public double? this[string courseCode] |
| 构造函数 | 每个类都有 | public Person(string name, ...) |
| LINQ | GetAverageScore、GetRanking | Scores.Average(s => s.Value) |
| 静态类 | ReportGenerator | public static class ReportGenerator |
| 对象初始化器 | 可以用于创建课程的 List(可选) | new Course { Code="CS101", ... } |
17. 扩展练习
基于这个系统,你可以做以下练习:
练习1:增加学期概念
给 Score 类增加 Semester 属性(如 "2026春"),修改成绩单显示按学期分组。
练习2:成绩排名优化
当前排名只按平均分。但如果两个学生平均分相同,应该按获得 A 的数量再排一次。请实现这个逻辑。
练习3:增加课程选修系统
给 Student 类增加一个 Enroll(Course course) 方法,让学生自己选课。只有选了的课才能录入成绩。
练习4:补考机制
如果一个学生的某门课成绩低于 60 分(F 等级),自动标记为"需补考",并添加一个 MakeupScore 属性记录补考成绩。
练习5:文件读写
用 System.IO.File 把班级报告保存为文本文件,文件名用 {班级名称}_报告_{日期}.txt 格式。
18. 小结
┌────────────────────────────────────────────────────────────┐
│ 综合案例核心要点 │
├────────────────────────────────────────────────────────────┤
│ 设计原则: 先分析需求, 再画类关系图, 最后写代码 │
│ │
│ 分层结构: 枚举 → 接口 → 抽象类 → 具体类 → 工具类 → Main │
│ │
│ 继承关系: Person(抽象) → Student / Teacher │
│ │
│ 接口实现: IIdentifiable → Person │
│ IStudy → Student │
│ │
│ 多态体现: List<Person> 中调用 Introduce() │
│ 每个对象按自己的方式执行 │
│ │
│ 数据关系: Teacher ──教──▶ Course ◀──成绩── Score │
│ ClassRoom ──包含──▶ Student(多个) │
│ │
│ 静态工具: ReportGenerator 不创建实例, 直接调用方法 │
│ │
│ 关键技巧: base() 调父类构造 / 索引器 / 事件 / LINQ │
└────────────────────────────────────────────────────────────┘
一句话总结
这个学生管理系统综合运用了 C# 类的几乎所有核心特性——从枚举到多态,从事件到索引器。先理解每个零件的作用,再观察它们如何协同工作。建议先把完整代码复制到 VS 里跑一遍,然后尝试做扩展练习,这是最快的进步方式。