C# 学习系列: C# 入门 → C# 基础 → C# 进阶 → C# 高级
接续《C# 入门》《C# 基础》《C# 进阶》。目标:从“能写类”走向“能组织集合、行为和运行时信息”,最后完成分层控制台俄罗斯方块。
配套项目目标框架:.NET 10,语言版本固定为 C# 12,启用可空引用类型检查。本文主体不依赖 C# 13/14 新语法;新增泛型约束单独说明。代码片段分为方法体片段、类型声明、完整文件,片段不应全部粘进同一个 Main。完整可运行代码见配套源码。方法体示例按需引入 System、System.Collections、System.Collections.Generic、System.Reflection、System.Threading;特定示例另列 using。涉及前文类型的片段应放在同一项目中。
目录与学习路线
简单数据结构类
泛型和约束
常见泛型数据结构类
委托和事件
List 排序
协变逆变
多线程
预处理指令
反射和特性
迭代器
特殊语法
值类型和引用类型二
项目实践:俄罗斯方块
练习与答案、官方资料、完整项目源码
先理解集合保存什么,再学习用泛型约束元素;委托把方法作为参数,事件把通知与处理分离;迭代器统一遍历;反射与特性实现启动时注册;最后用接口连接游戏的输入、规则、输出与统计。
环境与运行 安装 .NET 10 SDK 后,在解压目录执行:
1 2 3 dotnet --info dotnet run --project HighExamples/HighExamples.csproj dotnet run --project TetrisGame/TetrisGame.csproj
HighExamples 是章节演示与规则检查程序,TetrisGame 是交互游戏。不需要第三方 NuGet 包。游戏需真实交互终端,至少 73 列 × 33 行;推荐 Windows Terminal、PowerShell、macOS Terminal 或 Linux 终端,不要用只显示文本的输出面板。游戏使用 ASCII 界面以避免中文字符宽度造成错位。
第一章:简单数据结构类 1.1 本质:容器、元素类型与访问规则 这些类位于 System.Collections,属于非泛型集合,元素以 object 接口形态暴露。存入 int 等值类型通常发生装箱,取出时需要正确拆箱;混合类型会把错误延后到运行时。新项目优先使用对应泛型集合,本章用于理解历史代码和容器行为。
1 2 3 4 5 6 7 8 9 using System;using System.Collections; ArrayList values = new ArrayList(); values.Add(10 ); values.Add("ten" ); int first = (int )values[0 ]!; long converted = (int )values[0 ]!;
类
抽象规则
典型用途
泛型替代
ArrayList
动态长度、按索引访问
旧代码异构列表
List
Stack
后进先出 LIFO
撤销、回溯
Stack
Queue
先进先出 FIFO
等待、任务排队
Queue
Hashtable
按键定位值
映射、查表
Dictionary<TKey,TValue>
“动态长度”指容器内部按需扩容,不代表数组本身能改变长度。Count 是元素数;Capacity 是部分容器已分配容量,两者不同。复杂度描述默认以元素数 n 计,哈希操作平均 O(1)、最坏 O(n),不保证所有输入均常数时间。
1.2 ArrayList:声明、增删改查和遍历 底层采用动态数组思想。索引访问 O(1),尾部 Add 摊还 O(1),中间插入、删除要移动后续元素,为 O(n)。
1 2 3 4 5 6 7 8 9 10 11 12 13 ArrayList list = new ArrayList { 10 , 20 , 30 }; list.Add(40 ); list.AddRange(new int [] { 50 , 60 }); list.Insert(1 , 15 ); list[0 ] = 99 ; bool exists = list.Contains(30 );int index = list.IndexOf(30 ); object ? value = list[2 ]; list.Remove(20 ); list.RemoveAt(1 ); foreach (object ? item in list) Console.WriteLine(item);for (int i = 0 ; i < list.Count; i++) Console.WriteLine(list[i]); list.Clear();
索引必须满足 0 <= index < Count;Insert 可在 Count 位置插入。Remove(20) 与 RemoveAt(20) 含义不同。Sort() 要求元素可互相比较,混入字符串与整数可能失败。一般不要在 foreach 中增删原集合;可倒序 for 删除,或先记录要删除的元素。
1.3 Stack:后进先出 栈顶是唯一常规进出口。Push 压栈,Pop 弹出并删除,Peek 只看栈顶。底层通常采用可扩容数组;Push 摊还 O(1),Peek/Pop O(1),Contains O(n)。
1 2 3 4 5 6 7 8 Stack stack = new Stack(); stack.Push("第一步" ); stack.Push("第二步" ); Console.WriteLine(stack.Peek()); if (stack.Count > 0 ) Console.WriteLine(stack.Pop()); Console.WriteLine(stack.Contains("第一步" ));foreach (object ? item in stack) Console.WriteLine(item); stack.Clear();
空栈 Peek/Pop 抛 InvalidOperationException。非泛型 Stack 没有按索引修改或删除中间项的常规 API;如确有需要,应弹出、重建,或重新选择容器。不要把“增删改查”理解成每种容器都必须支持随机修改。
1.4 Queue:先进先出 队尾入队,队首出队,适合事件和任务排队。可用循环数组实现;Enqueue 摊还 O(1),Dequeue/Peek O(1),Contains O(n)。
1 2 3 4 5 6 7 8 Queue queue = new Queue(); queue.Enqueue("玩家A" ); queue.Enqueue("玩家B" ); Console.WriteLine(queue.Peek()); if (queue.Count > 0 ) Console.WriteLine(queue.Dequeue()); Console.WriteLine(queue.Contains("玩家B" ));foreach (object ? item in queue) Console.WriteLine(item); queue.Clear();
空队列的 Peek/Dequeue 会抛异常。它不提供索引器,也不支持直接修改队中第 k 项。游戏中可把“方块落地事件”按顺序入队,交给统计线程消费。
1.5 Hashtable:键值映射 哈希函数把键映射到桶,再用相等比较区分冲突。键应保持哈希与相等语义稳定;把参与 GetHashCode 的字段改掉,可能导致键还在表中却查不到。键不可为 null,值可以为 null。
1 2 3 4 5 6 7 8 9 10 11 12 Hashtable table = new Hashtable(); table.Add("Alice" , 100 ); table["Bob" ] = 80 ; table["Alice" ] = 120 ; if (table.ContainsKey("Alice" )) Console.WriteLine(table["Alice" ]);bool hasScore = table.ContainsValue(80 ); table.Remove("Bob" );foreach (DictionaryEntry entry in table) Console.WriteLine($"{entry.Key} : {entry.Value} " );foreach (object key in table.Keys) Console.WriteLine(key);foreach (object ? v in table.Values) Console.WriteLine(v); table.Clear();
读取不存在的键返回 null,不能仅凭结果 null 区分“键不存在”与“值是 null”。遍历顺序不应作为业务契约。以上集合默认均不适合多线程并发读写,线程安全需另外设计。
第二章:泛型和约束 2.1 概念、分类、作用 泛型把具体类型推迟为类型参数,仍保留编译期类型检查。List<int> 只能接收 int,避免非泛型集合频繁装箱和强制转换;但泛型不保证绝对没有装箱,例如把 T 转成 object 时仍可能装箱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public sealed class Box <T > { public T Value { get ; } public Box (T value ) => Value = value ; }public interface IRepository <T > { void Add (T value ) ; T Get (int index ) ; }public static class Tools { public static void Swap <T >(ref T a, ref T b ) { T temporary = a; a = b; b = temporary; } }
使用:Box<int> box = new Box<int>(3);;Tools.Swap(ref x, ref y) 通常可推断 T,也可显式写 Tools.Swap<int>。泛型方法需要自己声明类型参数;一个普通方法位于泛型类里,不代表该方法自身也是泛型方法。除此之外还有泛型结构体和泛型委托,例如 Nullable<T> 与 Func<T,TResult>。
2.2 传统六种约束 没有约束时,编译器不能假设 T 有某个业务方法。约束写在声明后,用 where 描述调用者必须满足的条件。
约束
含义
可依赖的能力
where T : struct
非可空值类型
值类型语义;隐含 new()
where T : class
引用类型
引用语义;可空上下文中要求非可空
where T : new()
有 public 无参构造函数的非抽象类型
可以 new T()
where T : Base
Base 或派生类型
可以使用 Base 的可访问成员
where T : IFoo
实现接口
可以调用接口成员;结构体也可满足
where T : U
T 可赋给 U 的类型约束
描述两个类型参数之间关系
1 2 3 4 5 6 7 8 9 10 11 12 public interface IHasId { string Id { get ; } }public class Entity : IHasId { public string Id { get ; set ; } = "entity" ; }public sealed class Player : Entity { }public sealed class Factory <T > where T : Entity , IHasId , new () { public T Create () => new T(); public string ReadId (T value ) => value .Id; }public sealed class Related <TBase , TDerived > where TDerived : TBase , new () { public TBase Create () => new TDerived(); }
多个约束用逗号连接,多个参数分别写 where。主约束在前,接口居中,new() 通常最后。struct 与 class 不能组合,struct 不能再写 new();基类约束不能与 class/struct 主约束同时使用。接口可有多个。约束不是运行时 if,也不能只靠约束差异区分两个签名相同的重载。
2.3 不止六种:补充与版本边界 传统六种是入门分类,并非现代 C# 完整清单。notnull 要求非可空类型(违反时通常产生可空分析警告);unmanaged 要求不含托管引用的非可空值类型,不能与 struct/new() 组合;Enum、Delegate 可作为约束;default 用于重写或显式接口实现中的特定约束消歧。C# 13 还有 allows ref struct 反约束,本文 C# 12 项目不使用。查看官方“类型参数约束”文档时应留意语言版本。
1 2 public static T Max <T >(T a, T b ) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;
约束应表达算法真正需要的能力。仅需比较时约束 IComparable,不必限制为某个具体业务类。
第三章:常见泛型数据结构类 3.1 List:动态数组 位于 System.Collections.Generic。逻辑上有序,重复元素合法;底层连续数组保存 T 的值,若 T 是引用类型,数组槽位保存对象引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 List<int > scores = new List<int >(capacity: 8 ) { 10 , 20 , 30 }; scores.Add(40 ); scores.AddRange(new [] { 50 , 60 }); scores.Insert(1 , 15 ); scores[0 ] = 99 ;int first = scores[0 ];bool found = scores.Contains(30 );int at = scores.IndexOf(30 );int firstLarge = scores.Find(x => x > 40 ); List<int > large = scores.FindAll(x => x > 40 ); scores.Remove(20 ); scores.RemoveAt(1 ); scores.RemoveAll(x => x < 20 );foreach (int score in scores) Console.WriteLine(score);for (int i = 0 ; i < scores.Count; i++) Console.WriteLine(scores[i]); scores.Clear();
Find 返回 default 可能与真实元素混淆,可用 FindIndex(未找到 -1)。Count 不是 Capacity,Clear 后容量通常保留。TrimExcess() 可请求压缩多余容量,但不应频繁调用。ToArray() 得到新的数组,元素仍是浅复制。
3.2 Dictionary<TKey,TValue> 泛型哈希表,键唯一。TKey 受 notnull 约束;使用字符串键时明确选择比较规则。
1 2 3 4 5 6 7 8 9 10 11 12 Dictionary<string , int > scores = new Dictionary<string , int >(StringComparer.OrdinalIgnoreCase); scores.Add("Alice" , 100 );bool added = scores.TryAdd("ALICE" , 200 ); scores["Bob" ] = 80 ; scores["Alice" ] = 120 ;if (scores.TryGetValue("alice" , out int score)) Console.WriteLine(score); Console.WriteLine(scores.ContainsKey("Bob" )); Console.WriteLine(scores.ContainsValue(80 ));bool removed = scores.Remove("Bob" );foreach (KeyValuePair<string , int > pair in scores) Console.WriteLine($"{pair.Key} ={pair.Value} " ); scores.Clear();
不存在的键使用索引器读取会抛 KeyNotFoundException,TryGetValue 更适合常见的查询失败。键比较由 IEqualityComparer 决定,Equals 相等的键必须返回相同哈希码,反过来不成立。不要用字典遍历顺序表达排名;排名先复制到 List 再排序。
3.3 顺序存储与链式存储
操作
List
带头尾指针的单向链表
LinkedList 双向链表
索引访问
O(1)
O(n)
不提供索引器,定位 O(n)
尾部增加
摊还 O(1)
O(1)
O(1)
头部增加/删除
O(n)
O(1)
O(1)
已知位置插入
移动后续项 O(n)
已知前驱 O(1)
已知节点 O(1)
按值查找
O(n)
O(n)
O(n)
空间局部性
较好
较差,节点有额外引用
较差,含前后引用
链表插入快的前提是已经定位到合适节点,按值找节点的成本不能忽略。逻辑相邻不代表内存相邻。
flowchart TD
H["head"] --> A["节点 A:值 + next"]
A --> B["节点 B:值 + next"]
B --> C["节点 C:值 + null"]
T["tail"] --> C
3.4 实现单向链表 配套 Collections/SinglyLinkedList.cs 提供完整实现,含边界检查、Count、头尾节点、索引器、插入删除、查询、清空及标准枚举器。先看关键算法(类内部片段):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private sealed class Node { public T Value; public Node? Next; public Node (T value ) => Value = value ; }private Node? head, tail;public int Count { get ; private set ; }public void AddLast (T value ) { Node node = new Node(value ); if (tail is null ) head = node; else tail.Next = node; tail = node; Count++; }
插入到中间位置时,先保存旧后继,再重接引用:previous.Next = new Node(value) { Next = previous.Next };。删除时 previous.Next = previous.Next.Next,删除尾节点还要把 tail 指回 previous;删除最后一个节点必须同时清空 head 与 tail。完整实现维护 version,在遍历期间修改集合会抛异常。
1 2 3 4 5 6 7 8 SinglyLinkedList<int > list = new SinglyLinkedList<int >(); list.AddFirst(20 ); list.AddLast(40 ); list.Insert(1 , 30 ); list[0 ] = 10 ; Console.WriteLine(list.IndexOf(30 )); if (list.TryGet(2 , out int value )) Console.WriteLine(value ); list.Remove(30 ); list.RemoveAt(0 );foreach (int item in list) Console.WriteLine(item); list.Clear();
3.5 LinkedList:双向链表 系统类的节点为 LinkedListNode<T>,具有 Value、Previous、Next、List。拿到节点后能常数时间在前后插入或删除。它既不是 List 的子类,也没有 [index] 索引器。
1 2 3 4 5 6 7 8 9 10 11 12 13 LinkedList<string > route = new LinkedList<string >(); LinkedListNode<string > a = route.AddLast("A" ); LinkedListNode<string > b = route.AddAfter(a, "B" ); route.AddBefore(b, "X" ); route.AddFirst("Start" ); b.Value = "B2" ; LinkedListNode<string >? found = route.Find("X" );if (found is not null ) route.Remove(found); route.Remove("A" );foreach (string stop in route) Console.WriteLine(stop);for (LinkedListNode<string >? node = route.First; node is not null ; node = node.Next) Console.WriteLine(node.Value); route.RemoveFirst(); route.RemoveLast(); route.Clear();
RemoveFirst/RemoveLast 在空链表上抛异常。节点不能同时属于多个链表;删除后可将脱离的节点加入另一个链表。修改节点 Value 与改变链结构是不同操作;不要依赖不同集合对遍历期修改的差异,统一采用“先收集,再修改”的清晰策略。
第四章:委托和事件 4.1 委托的本质、声明与使用 委托是类型安全的方法调用对象。一个委托类型规定参数和返回值形状;一个委托实例持有目标对象与方法组成的调用列表。实例方法需要目标对象,静态方法通常不需要。声明类型、定义方法、创建实例、调用实例是四个步骤。
1 2 3 4 5 6 7 8 9 10 11 12 public delegate int Calculator (int a, int b ) ; public static class DelegateLesson { private static int Add (int a, int b ) => a + b; public static void Run () { Calculator operation = Add; Calculator other = new Calculator(Add); Console.WriteLine(operation(2 , 3 )); Console.WriteLine(other.Invoke(4 , 5 )); } }
委托让调用者提供变化的行为,例如“按哪种规则排序”,而不是在算法中写死。委托本身是引用类型,赋值不等于执行方法。
4.2 多播机制 通常所说“多播系统”是委托的多播调用机制:用 += 组合,用 -= 移除一个匹配的订阅,调用时按列表顺序同步执行。
1 2 3 4 5 6 7 8 Action? notify = null ; Action first = () => Console.WriteLine("保存" ); Action second = () => Console.WriteLine("刷新" ); notify += first; notify += second; notify?.Invoke(); notify -= first; notify?.Invoke();
委托不可变,组合产生新的委托值。重复订阅会重复调用;移除通常移除最后一个匹配项。普通调用中某个处理器抛异常,后续处理器不会继续执行。非 void 多播委托只能直接得到最后一个处理器的返回值,需要收集所有结果时逐个遍历 GetInvocationList()。多播默认不产生多线程。
4.3 系统委托
类型
形状
例子
Action
无返回值
Action<string> 接收一个 string
Func
最后一个泛型参数是返回类型
Func<int,int,int> 两个 int 输入、一个 int 输出
Predicate
T → bool
List.Find/RemoveAll 的判断
Comparison
(T,T) → int
List.Sort 的比较
EventHandler
(sender,args) → void
事件通知
1 2 3 4 Action<string > print = text => Console.WriteLine(text); Func<int , int , int > sum = (a, b) => a + b; Predicate<int > positive = x => x > 0 ; Comparison<int > ascending = (a, b) => a.CompareTo(b);
这些已经够用时,不必为每个方法形状创建新委托。业务语义特别明确时,自定义委托仍有价值。
4.4 事件:限制外部调用权 事件基于委托提供订阅/取消订阅机制。外部代码只能 +=、-=,不能覆盖整个调用列表,也不能直接触发事件。发布者决定何时发生,订阅者决定怎样响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public sealed class ScoreEventArgs : EventArgs { public int Added { get ; } public ScoreEventArgs (int added ) => Added = added; }public sealed class ScoreBoard { public event EventHandler<ScoreEventArgs>? ScoreChanged; public int Score { get ; private set ; } public void Add (int points ) { if (points < 0 ) throw new ArgumentOutOfRangeException(nameof (points)); Score += points; ScoreChanged?.Invoke(this , new ScoreEventArgs(points)); } }
订阅与取消(方法体片段):
1 2 3 4 5 ScoreBoard board = new ScoreBoard(); EventHandler<ScoreEventArgs> handler = (sender, e) => Console.WriteLine($"增加 {e.Added} " ); board.ScoreChanged += handler; board.Add(100 ); board.ScoreChanged -= handler;
长寿命发布者可能通过委托引用让订阅者保持可达,应在场景退出、对象释放时取消订阅。若以后要取消匿名处理器,先保存委托变量,重新写一段看似相同的 lambda 不应被当成可靠取消方式。?.Invoke 避免一次空检查与调用间委托字段变化造成的空引用,但不会自动保证业务数据线程安全。
俄罗斯方块在锁定后发布 PieceLocked,游戏场景写日志,统计服务收集数据;规则层不必知道日志如何显示。
4.5 匿名函数和 lambda 匿名方法:Func<int,int> square = delegate(int x) { return x*x; };。Lambda 可用表达式体 x => x*x,或语句体 x => { return x*x; }。无参数写 () => ...,多个参数加括号。
闭包捕获的是变量,不是声明时的一份固定快照:
1 2 3 4 5 6 7 List<Action> actions = new List<Action>();for (int i = 0 ; i < 3 ; i++) { int copy = i; actions.Add(() => Console.WriteLine(copy)); }foreach (Action action in actions) action();
若直接捕获 for 的 i,循环后调用时通常都看到 3。闭包可能延长局部变量及其引用对象的存活时间。static lambda 禁止捕获外部局部状态,例如 static x => x > 0,有助于看清依赖。Lambda 可转成委托,某些场景也可转成表达式树,两者用途不同,本教程以委托为主。
第五章:List 排序 5.1 自带 Sort 与 Reverse Sort() 按默认比较器原地升序排列,元素需支持默认比较。Reverse() 只反转当前顺序,并不负责把无序列表变成降序。Sort(index,count,comparer) 可排序局部区间。
1 2 3 List<int > values = new List<int > { 8 , 1 , 3 }; values.Sort(); values.Sort((a, b) => b.CompareTo(a));
比较函数结果只看符号:负数表示 a 在前,0 表示同序,正数表示 a 在后。不要写 a-b 比较所有整数,极值相减可能溢出。比较应自洽且具有传递性,不应依赖每次变化的随机值。
5.2 自定义类排序 有天然默认顺序时实现 IComparable<T>;同一对象有多种排序规则时,用 IComparer<T> 或 Comparison。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public sealed class PlayerScore : IComparable <PlayerScore > { public string Name { get ; } public int Score { get ; } public PlayerScore (string name, int score ) { Name = name; Score = score; } public int CompareTo (PlayerScore? other ) { if (other is null ) return 1 ; int result = other.Score.CompareTo(Score); return result != 0 ? result : StringComparer.Ordinal.Compare(Name, other.Name); } }public sealed class NameComparer : IComparer <PlayerScore > { public int Compare (PlayerScore? x, PlayerScore? y ) { if (ReferenceEquals(x, y)) return 0 ; if (x is null ) return -1 ; if (y is null ) return 1 ; return StringComparer.Ordinal.Compare(x.Name, y.Name); } }
1 2 3 4 5 6 7 List<PlayerScore> players = new List<PlayerScore> { new PlayerScore("Bob" , 100 ), new PlayerScore("Alice" , 100 ), new PlayerScore("Carol" , 200 ) }; players.Sort(); players.Sort(new NameComparer()); players.Sort((a, b) => b.Score.CompareTo(a.Score));
List<T>.Sort 是不稳定排序,不承诺同分对象仍保持原先相对顺序。需要稳定性时可使用 LINQ OrderBy/ThenBy 后 ToList()(需要 System.Linq),或显式把原始索引作为最终比较条件。游戏的方块统计先按次数降序,再按枚举值排序,确保同分显示确定。
第六章:协变逆变 6.1 从替换能力理解 假设 Dog 继承 Animal。一个 Dog 可放到需要 Animal 的位置,这是普通引用转换。泛型不自动把这种关系传递到所有容器:List<Dog> 不能赋给 List<Animal>,否则接收方就可能向狗列表添加猫。
协变保留方向:生产 Dog 的来源可当作生产 Animal 的来源。逆变反转方向:能够处理所有 Animal 的处理器,当然能处理 Dog。它们体现“在使用处可安全替换”,不能把里氏替换原则等同于 is/as 类型转换。
6.2 out 协变与 in 逆变 1 2 3 4 5 6 7 8 9 10 11 12 public class Animal { public string Name { get ; set ; } = "animal" ; }public sealed class Dog : Animal { }public interface ISource <out T > { T Get () ; }public interface ISink <in T > { void Put (T value ) ; }public sealed class DogSource : ISource <Dog > { public Dog Get () => new Dog(); }public sealed class AnimalSink : ISink <Animal > { public void Put (Animal value ) => Console.WriteLine(value .Name); }
1 2 3 4 5 6 7 8 9 10 ISource<Dog> dogSource = new DogSource(); ISource<Animal> animalSource = dogSource; Animal animal = animalSource.Get(); ISink<Animal> allAnimalSink = new AnimalSink(); ISink<Dog> dogSink = allAnimalSink; dogSink.Put(new Dog()); Func<Dog> makeDog = () => new Dog(); Func<Animal> makeAnimal = makeDog; Action<Animal> inspect = a => Console.WriteLine(a.Name); Action<Dog> inspectDog = inspect;
out/in 变体标注仅用于泛型接口和泛型委托的类型参数,不能直接标注泛型类。这不是 ref/out 传参语法。简单记忆:out 主要向外产出 T;in 主要消费 T,复杂嵌套还有更精确的位置规则。
变体转换针对引用类型参数:IEnumerable<int> 不能协变成 IEnumerable<object>,需要逐个投影并装箱。IEnumerable<Dog> 可转换为 IEnumerable<Animal>,即使实际底层对象是 List,也不意味着 List 类型本身变成协变。数组有历史性的引用协变,但向 object[] a = new string[1] 写整数会在运行时抛 ArrayTypeMismatchException。
第七章:多线程 7.1 进程、线程、并发与并行 进程是操作系统管理程序资源与隔离的单位;线程是执行流,同一进程中的线程通常共享托管堆与静态数据,各有调用栈。并发是多个任务交错推进,并行是同时执行。单核也能并发,多线程不保证更快,锁、切换和调度都存在成本。
游戏主线程负责输入、规则和绘制,避免两个线程同时修改棋盘或写控制台。后台线程仅接收落地信息并更新统计,用锁保护队列和结果。
7.2 声明、启动、后台与休眠 1 2 3 4 5 6 7 8 9 10 11 using System.Threading; Thread worker = new Thread(() => { Console.WriteLine("工作开始" ); Thread.Sleep(50 ); }); worker.Name = "demo" ; worker.IsBackground = true ; worker.Start(); worker.Join();
前台线程会让进程继续存活;只剩后台线程时进程可结束,后台工作不保证完成。因此后台线程也需明确停止和 Join。Sleep 不释放已持有的 lock,不是可靠的同步协议。Join 不应在持有工作线程需要的锁时调用,否则可能死锁。
7.3 释放:协作退出而非强制销毁 Thread 本身不实现 IDisposable,没有通用的“释放线程”方法。让入口函数返回,线程就结束;之后线程对象由 GC 管理。不要用 Abort 作为退出方案,现代 .NET 不支持它。用取消令牌、停止标记和信号实现退出,最后释放自己创建的同步资源。
1 2 3 4 5 6 7 8 9 10 11 12 using CancellationTokenSource stop = new CancellationTokenSource(); Thread worker = new Thread(() => { while (!stop.Token.WaitHandle.WaitOne(30 )) { } }) { IsBackground = true }; worker.Start(); Thread.Sleep(100 ); stop.Cancel(); worker.Join();
真实工作线程还要捕获并报告预期异常,不能让错误静默丢失;不要吞掉所有异常继续运行损坏状态。
7.4 共享数据与同步 counter++ 是读、计算、写,不是单个原子操作。
1 2 3 4 5 int counter = 0 ; Thread first = new Thread(() => { for (int i = 0 ; i < 10000 ; i++) Interlocked.Increment(ref counter); }); Thread second = new Thread(() => { for (int i = 0 ; i < 10000 ; i++) Interlocked.Increment(ref counter); }); first.Start(); second.Start(); first.Join(); second.Join(); Console.WriteLine(counter);
工具
适用场景
注意
lock
多字段不变量、普通集合读写
所有相关访问使用同一个锁;锁对象私有且稳定
Interlocked
计数、交换等简单原子操作
不能自动保护多步业务事务
volatile
部分字段的可见性与顺序保证
不会使 ++ 或集合操作原子化
AutoResetEvent
通知有新工作
信号可合并;真实数据仍放队列
ConcurrentQueue
并发入队出队
多步流程仍要设计完成条件
Task/线程池
通常的后台任务
异步 I/O 通常优先 async/await
不要锁字符串、typeof 或公开的 this,以免外部代码使用相同锁。避免持锁调用未知事件处理器,减少重入和死锁风险。集合的“安全遍历”可先在锁内复制快照,再在锁外排序和显示。游戏统计线程采用这条规则,结束时先停止接收、清空队列、Join,再读取最终结果。
第八章:预处理指令 预处理指令影响编译器如何处理源码,并非运行期分支。C# 没有 C/C++ 式文本宏替换,#define 只定义符号,不保存任意数字值。指令通常单独占一行,无分号。
指令
作用
场景
#define / #undef
定义、取消当前文件条件符号
必须位于普通代码之前
#if / #elif / #else / #endif
条件编译
DEBUG、平台或自定义符号
#region / #endregion
编辑器折叠区域
不改变执行结果
#warning / #error
编译警告、编译错误
配置检查
#nullable enable/disable/restore
控制可空分析上下文
新旧代码迁移
#pragma warning disable/restore
控制指定编译警告
应限制作用范围
#line
调整报告的源码位置
生成代码
#pragma checksum
提供源码校验信息
调试和生成工具
1 2 3 4 5 6 7 8 9 10 #define LOCAL_TRACE using System;#if DEBUG && LOCAL_TRACE Console.WriteLine("调试构建,启用本文件日志" );#elif DEBUG Console.WriteLine("调试构建" );#else Console.WriteLine("发布构建" );#endif
项目级定义可写在 csproj 的 <DefineConstants>$(DefineConstants);GAME_TRACE</DefineConstants>。DEBUG 通常由 Debug 配置提供,不能仅靠“正在调试”判断符号是否存在。运行 dotnet build -c Release 可检查另一条分支。被排除的分支不会生成对应 IL,应对重要配置分别编译验证。
#if 与 [Conditional("DEBUG")] 不同:前者排除代码,后者主要控制调用点是否保留,该方法通常仍存在于程序集。#nullable 不会在运行时自动阻止 null,也不改变对象布局。
第九章:反射和特性 9.1 程序集、元数据与反射 程序集是 .NET 可部署、可版本化的构建单元,常见为托管 dll;其中包含清单、类型和成员元数据,以及通常的 IL 方法体。命名空间是逻辑命名组织,不等于程序集,也不等于文件夹。原生 DLL 不一定是 .NET 程序集,不能一律用 Assembly.Load 加载。
元数据描述“有哪些类型、成员、参数和标记”。反射在运行时读取这些描述,也可构造对象、读写成员、调用方法。用途包括编辑器检查器、序列化、测试发现、插件注册;代价包括运行时错误、查找开销、裁剪/AOT 兼容问题。固定依赖应优先直接调用或接口,反射适合启动阶段发现,结果缓存后使用。
flowchart TD
A["Assembly:程序集"] --> T["Type:类型描述"]
T --> M["MethodInfo / PropertyInfo / FieldInfo"]
T --> C["ConstructorInfo"]
T --> X["Attribute:附加元数据"]
9.2 Type:它描述类型,而不是业务对象 Type 自身是抽象类,运行时给出具体实现。获取方式:typeof(Player)、instance.GetType()、Type.GetType("命名空间.类型, 程序集名")。GetType 不可直接对 null 调用;typeof 获取编译期指定类型,实例 GetType 获取实际运行时类型。
成员/典型签名
返回值、含义
使用注意
Name / FullName / AssemblyQualifiedName
简名、带命名空间名、带程序集身份名
部分类型 FullName 可为 null
BaseType / Assembly
父类型描述、所在程序集
接口的 BaseType 为 null
IsClass / IsInterface / IsValueType / IsEnum
类型类别标记
string 是类,数组是引用类型
IsAbstract / IsSealed
是否抽象、密封
静态类通常两者都为 true
IsGenericType / IsGenericTypeDefinition
是否泛型、是否开放泛型定义
List 与 List<> 有区别
GetGenericArguments()
Type[],泛型参数或实参
List 得到 int
GetGenericTypeDefinition()
泛型类型定义
非泛型类型调用会失败
MakeGenericType(params Type[])
以实参构造泛型 Type
在泛型定义上调用,需满足约束
GetInterfaces()
Type[],实现的接口
包括继承得到的接口
IsAssignableFrom(Type)
来源类型是否可赋给当前类型
typeof(Animal).IsAssignableFrom(typeof(Dog))
IsInstanceOfType(object?)
对象能否赋给该类型变量
null 返回 false
GetConstructor(Type[])
ConstructorInfo?
用参数类型消除重载歧义
GetMethod(string,Type[])
MethodInfo?
精确查找某个重载
GetMethods(BindingFlags)
MethodInfo[]
明确 public/nonpublic、instance/static
GetProperty(string) / GetField(string)
PropertyInfo? / FieldInfo?
没找到返回 null
GetProperties() / GetFields() / GetEvents()
对应成员描述数组
默认主要查 public 成员
BindingFlags 是枚举,可组合 BindingFlags.Public | BindingFlags.Instance。仅写 NonPublic 没有 Instance/Static,往往得不到预期结果。非公开继承成员的查找规则较细,特别是基类 private 成员,不能理解为“加 NonPublic 就能查到全部”。
完整反射示例(独立文件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 using System;using System.Reflection;public sealed class ReflectPlayer { public string Name { get ; set ; } public int Score; public ReflectPlayer (string name ) => Name = name; public int Add (int points ) { Score += points; return Score; } }public static class ReflectionLesson { public static void Run () { Type type = typeof (ReflectPlayer); Console.WriteLine(type.FullName); ConstructorInfo ctor = type.GetConstructor(new [] { typeof (string ) }) ?? throw new MissingMethodException(type.FullName, ".ctor" ); object player = ctor.Invoke(new object [] { "Alice" }); PropertyInfo name = type.GetProperty(nameof (ReflectPlayer.Name))!; name.SetValue(player, "Bob" ); Console.WriteLine(name.GetValue(player)); FieldInfo score = type.GetField(nameof (ReflectPlayer.Score))!; score.SetValue(player, 10 ); MethodInfo add = type.GetMethod(nameof (ReflectPlayer.Add), new [] { typeof (int ) })!; object ? result = add .Invoke(player, new object [] { 5 }); Console.WriteLine(result); Type closed = typeof (System.Collections.Generic.List<>).MakeGenericType(typeof (int )); Console.WriteLine(closed.IsGenericType); } }
MethodInfo.Invoke(object? obj, object?[]? parameters):实例方法传实例,静态方法传 null;结果为 object?,void 返回 null。参数类型/个数不匹配会失败,业务方法内部异常通常包装在 TargetInvocationException.InnerException 中。示例用 ! 是基于已知类型成员存在,不适合把任意配置都不检查地强制解引用。
9.3 Assembly:查找与加载托管程序集
API
含义
示例/注意
typeof(T).Assembly
已知类型所在程序集
最直接可靠的定位方式
Assembly.GetExecutingAssembly()
当前执行代码所在程序集
不等于永远是入口程序
Assembly.GetEntryAssembly()
进程入口托管程序集
某些宿主环境可为 null
Assembly.GetCallingAssembly()
调用方程序集
内联等会影响,不宜作安全判据
Assembly.GetName()
AssemblyName,含名称版本等
FullName 是完整身份
Assembly.Load(AssemblyName/string)
按身份请求加载
依赖运行时加载上下文
Assembly.LoadFrom(string path)
从文件路径加载
应用信任边界内使用
GetTypes()
全部定义类型
依赖缺失可能抛 ReflectionTypeLoadException
GetExportedTypes()
对外可见的类型
不等于全部类型
GetType(string,bool)
按完整名查询类型
不存在可返回 null 或抛错
CreateInstance(string)
按类型名构造
通常依赖公开无参构造
GetManifestResourceNames()
内嵌资源名数组
名称一般含命名空间
GetManifestResourceStream(string)
Stream?
找不到返回 null;使用后 Dispose
1 2 3 4 Assembly assembly = typeof (ReflectPlayer).Assembly; Console.WriteLine(assembly.GetName().Name);foreach (Type type in assembly.GetTypes()) Console.WriteLine(type.FullName); Type? target = assembly.GetType(typeof (ReflectPlayer).FullName!, throwOnError: false );
Assembly.LoadFrom("Plugins/MyPlugin.dll") 不是沙箱,也不能安全执行不可信代码。需要可卸载插件时使用专门的 AssemblyLoadContext,单独的 Assembly 没有通用 Unload 方法。程序集加载、文件路径、类型发现异常需要报告上下文,不能直接忽略“加载失败的类型”。游戏只扫描自身程序集,避免无关插件加载的复杂性。
9.4 Activator:根据运行时类型构造对象 Activator 是静态工厂工具,不能 new Activator。
API
作用
条件/返回
CreateInstance()
按泛型类型构造
通常要求可用无参构造,否则运行时失败
CreateInstance(Type)
按 Type 构造
object?;不可直接实例化接口/抽象类
CreateInstance(Type, object?[]?)
匹配参数构造
反射选择构造重载,错误会在运行时出现
CreateInstance(Type, bool nonPublic)
可允许非公开无参构造
不应借此破坏业务边界
CreateInstance(Type, BindingFlags, Binder?, object?[]?, CultureInfo?)
控制绑定
高级重载,通常不需要
CreateInstance(string,string)
按程序集名和类型名创建
部分重载返回 ObjectHandle,需 Unwrap;现代代码通常优先 Type 重载
1 2 3 Type playerType = typeof (ReflectPlayer);object ? raw = Activator.CreateInstance(playerType, new object [] { "Alice" });if (raw is ReflectPlayer player) Console.WriteLine(player.Name);
找不到匹配构造常见 MissingMethodException,构造内部错误常见 TargetInvocationException。只需要编译期可确定的构造时 new Player() 最清晰;泛型工厂可用 where T : new() 避免反射。本章演示 API 的常用重载,不使用依赖远程处理的旧框架专用接口。
9.5 特性:附加元数据 特性是继承 Attribute 的类。把 [标记(...)] 写在声明上会记录元数据;标记本身通常不会“自动运行规则”,必须由编译器、运行时、框架或你的反射代码读取和解释。
1 2 3 4 5 6 7 8 9 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false) ]public sealed class PluginAttribute : Attribute { public string Name { get ; } public int Order { get ; set ; } public PluginAttribute (string name ) => Name = name; } [Plugin("basic" , Order = 1) ]public sealed class BasicPlugin { }
名称末尾 Attribute 在使用时可省略。构造参数是位置参数;可写公共属性/字段可作为命名参数。参数类型受限制,常见为基本数值类型、bool、char、string、Type、枚举和符合规则的一维数组,不能随意传 DateTime、decimal 或业务对象。AttributeUsage 控制合法目标、是否允许多次、是否继承。
1 2 PluginAttribute? metadata = typeof (BasicPlugin).GetCustomAttribute<PluginAttribute>(); Console.WriteLine(metadata?.Name);
GetCustomAttribute 会实例化特性;仅检查元数据而不执行特性构造,可使用 CustomAttributeData。游戏在方块定义类上标记 PieceAttribute,再过滤接口实现并用 Activator 创建,最后存入 Dictionary。
9.6 系统自带特性 Obsolete:过时 API 1 2 3 [Obsolete("请使用 NewSave 方法" , error: false) ]public static void OldSave () { }public static void NewSave () { }
false 触发编译警告,true 通常阻止调用代码编译;不会自动转发到新方法。应提供替代方式和迁移说明。
调用者信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 using System.Runtime.CompilerServices;public static void Log (string text, [CallerMemberName] string member = "" , [CallerFilePath] string file = "" , [CallerLineNumber] int line = 0 ) { Console.WriteLine($"{member} :{line} {text} " ); }public static void Require (bool condition, [CallerArgumentExpression("condition" )] string expression = "" ) { if (!condition) throw new ArgumentException(expression); }
放在有默认值的参数上,调用者省略该实参时由编译器填入。CallerArgumentExpression 可获得实参表达式文本;不是运行时扫描调用栈。源码路径可能含构建机信息,生产日志应按需处理。
Conditional:条件调用 1 2 3 4 using System.Diagnostics; [Conditional("DEBUG" ) ]public static void Trace (string message ) => Console.WriteLine(message);
方法需返回 void;调用点没有 DEBUG 符号时,调用及参数求值通常都被省略。因此不要把重要副作用放在日志参数里。该方法仍可出现在程序集,也不能用它代替权限检查。特性声明在被调用方法上,但是否保留调用由调用方的编译符号决定。
DllImport:外部原生 DLL 函数 1 2 3 4 5 6 7 8 9 10 11 12 using System.Runtime.InteropServices;public static class NativeMethods { [DllImport("kernel32.dll" , EntryPoint = "GetCurrentProcessId" , ExactSpelling = true) ] private static extern uint GetCurrentProcessId () ; public static uint CurrentProcessId () { if (!OperatingSystem.IsWindows()) throw new PlatformNotSupportedException("Windows only" ); return GetCurrentProcessId(); } }
这是 Windows 原生函数示例,游戏不依赖它。extern 表示实现在外部,DllImport 指明库名、入口和封送约定。现代 .NET 也提供 LibraryImport 源生成方案。字符串字符集、调用约定、整数宽度、布尔布局与资源所有权必须匹配原生签名,否则可能崩溃;失败还可能是位数、库路径或入口不存在。调用托管 C# DLL 则通常添加项目/程序集引用,不用 DllImport。上述功能也可直接用 Environment.ProcessId 获取,P/Invoke 示例用于理解机制。
第十章:迭代器 10.1 可枚举对象与枚举器 IEnumerable<T> 表示“能提供枚举器”;IEnumerator<T> 表示“一次遍历的当前位置状态”。集合可被多次遍历,每次 GetEnumerator 通常创建独立枚举器。
foreach 的概念过程:取得枚举器;反复 MoveNext;成功后读 Current;最后 Dispose。首次 MoveNext 前没有有效当前项,结束后也没有有效当前项。
10.2 手工实现标准迭代器 完整示例(独立类型文件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 using System;using System.Collections;using System.Collections.Generic;public sealed class NumberRange : IEnumerable <int > { private readonly int count; public NumberRange (int count ) { if (count < 0 ) throw new ArgumentOutOfRangeException(nameof (count)); this .count = count; } public IEnumerator<int > GetEnumerator () => new Cursor(count); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); private sealed class Cursor : IEnumerator <int > { private readonly int count; private int index = -1 ; public Cursor (int count ) => this .count = count; public int Current => index >= 0 && index < count ? index : throw new InvalidOperationException(); object IEnumerator.Current => Current; public bool MoveNext () { if (index < count) index++; return index < count; } public void Reset () => index = -1 ; public void Dispose () { } } }
foreach (int n in new NumberRange(3)) 输出 0、1、2。Reset 不是 foreach 必需能力,很多框架枚举器直接抛 NotSupportedException。这里支持它便于教学。配套单向链表的枚举器额外检测版本变化,防止在改动的结构上继续遍历。
10.3 yield return 语法糖 1 2 3 4 5 6 7 8 9 10 11 12 public static IEnumerable<int > Range (int count ) { for (int i = 0 ; i < count; i++) yield return i; }public static IEnumerable<int > UntilNegative (IEnumerable<int > values ) { foreach (int value in values) { if (value < 0 ) yield break ; yield return value ; } }
编译器生成状态机,保存执行位置和跨暂停的局部变量。每次 MoveNext 推进到下一次 yield return;yield break 结束。返回 IEnumerable 不等于立即运行完方法,通常遍历才执行主体,异常也可能到遍历时才抛。
如果需要在调用时就验证参数,把验证和迭代主体拆开:
1 2 3 4 5 6 7 8 9 public static IEnumerable<int > CheckedRange (int count ) { if (count < 0 ) throw new ArgumentOutOfRangeException(nameof (count)); return Iterate(count); static IEnumerable<int > Iterate (int n ) { for (int i = 0 ; i < n; i++) yield return i; } }
迭代器可包含 try/finally,foreach 提前 break 时通常会 Dispose 枚举器,执行清理;不能在 catch/finally 中 yield return,也不能在带 catch 的 try 中 yield return。本文不扩展异步迭代器。
10.4 为泛型类实现迭代器 1 2 3 4 5 6 7 8 9 10 public sealed class Shelf <T > : IEnumerable <T > { private readonly List<T> items = new List<T>(); public void Add (T item ) => items.Add(item); public IEnumerator<T> GetEnumerator () { foreach (T item in items) yield return item; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
Shelf<string>、Shelf<int> 复用同一套遍历逻辑。yield 不自动提供快照和线程安全;底层 List 在遍历中修改仍可能抛异常。游戏 Board 用 yield 按行遍历已锁定格子,渲染器不需要知道内部二维数组;活动方块同样逐格产出坐标。
第十一章:特殊语法 11.1 var 隐式局部类型 1 2 3 var count = 3 ; var name = "Alice" ; var map = new Dictionary<string , int >();
var 不是 dynamic,类型推断后不变,不能随后把 count 赋成字符串。局部变量需要能推断类型的初始化表达式;var x = null 不行。它不是成员字段、普通方法参数或返回类型的通用替代。
11.2 对象与集合初始值 1 2 3 4 5 public sealed class Hero { public string Name { get ; set ; } = "" ; public int Level { get ; set ; } }
1 2 3 4 5 6 7 8 9 10 Hero hero = new Hero { Name = "Alice" , Level = 1 }; List<Hero> heroes = new List<Hero> { hero, new Hero { Name = "Bob" , Level = 2 } }; Dictionary<string , int > scores = new Dictionary<string , int > { { "Alice" , 10 }, { "Bob" , 20 } }; Dictionary<string , int > otherScores = new Dictionary<string , int > { ["Alice" ] = 10 , ["Bob" ] = 20 };
先执行构造,再赋初始属性/字段;访问限制依然生效。集合初始值通常依赖 IEnumerable 和可用 Add 方法;字典 Add 形式遇重复键抛异常,索引形式可覆盖前值;同一个初始值列表不要混合这两种形式。C# 12 集合表达式 int[] a = [1,2,3] 属于另一种语法,不应与传统 { ... } 初始值混为一谈。
11.3 匿名类型 1 2 var summary = new { Name = "Alice" , Score = 100 }; Console.WriteLine($"{summary.Name} : {summary.Score} " );
编译器生成带只读属性的引用类型,适合方法内临时投影。同一程序集、属性名/顺序/类型相同的匿名类型可共享生成类型。不是动态对象,不适合充当稳定公共 API;需要跨模块传递时定义有名类或 record。属性只读不代表其引用对象深度不可变。
11.4 可空类型、空条件、空合并 1 2 3 4 5 6 int ? score = null ; Console.WriteLine(score.HasValue); int display = score ?? 0 ;string ? name = null ; name ??= "游客" ;int length = name?.Length ?? 0 ;
Nullable 仅用于非可空值类型;Value 在没有值时抛异常。?? 在左侧非 null 时返回左侧,否则求值右侧;??= 仅当左侧为 null 时赋值。?. 遇 null 短路访问链,值类型结果通常提升为可空。空合并不是“空字符串合并”,"" ?? "默认" 仍得到空字符串。后缀 ! 只影响编译器分析,不是运行期 null 检查。
11.5 内插字符串与简写 1 2 3 4 int points = 123 ;string label = $"分数:{points:D6} " ; Console.WriteLine($"{ "Name" ,-10 } {points,8 } " ); string state = points > 100 ? "高分" : "继续" ;
数值格式可能受区域设置影响,保存机器数据时可明确使用 InvariantCulture。三元表达式适合简单二选一,深层嵌套会降低可读性。
表达式体成员:public int Double(int x) => x * 2;。它是成员语法,不是 lambda 委托。单条 if/for/while 可省大括号,但嵌套时 else 绑定最近未匹配 if,教学和维护建议保留括号。
1 2 3 4 5 6 7 public static string Describe (int lines ) => lines switch { 0 => "没有消行" , 1 => "单行" , 2 => "双行" , _ => "多行" };
switch 表达式必须产出值,_ 常作兜底。逻辑简写应保持语义:bool 结果通常直接 return score > 0;,不必再写 ? true : false。简写不是目标,清楚地表达规则才是。
第十二章:值类型和引用类型二 12.1 如何判断 值类型:基本数值、bool、char、enum、struct、值元组、Nullable。引用类型:class、string、数组、delegate、interface 变量所持有的对象引用。接口本身不是某种固定值布局,值类型转接口通常装箱。
运行时可用 typeof(T).IsValueType 或对象 GetType().IsValueType;后者若来自装箱 int,仍返回 true,因为描述的是被装箱的实际类型。null 没有可调用的 GetType。
判断核心在赋值复制语义,不在“栈还是堆”:值类型变量复制其值,引用类型变量复制引用。值类型也可内嵌于堆对象、数组或被装箱;局部变量也可能被闭包提升,JIT 还可把值放寄存器。
12.2 语句块、作用域和生命周期 作用域是源码中可以按名称访问变量的区域;生命周期是运行时存储/对象持续存在的时间。两者不是同一件事。
1 2 3 4 5 6 Action saved; { int count = 0 ; saved = () => Console.WriteLine(++count); } saved(); saved();
局部变量在使用前需明确赋值,字段/数组元素通常有默认值。代码块结束不等于对象立即回收;对象无强引用可达后才有资格被 GC 回收,回收时刻不确定。using/Dispose 用于确定性释放资源,不是直接销毁所有托管对象。静态字段通常随加载上下文/进程存活,可能长期保持对象可达。
12.3 结构体中的值和引用 1 2 3 4 5 public struct Bag { public int Gold; public List<string > Items; }
1 2 3 4 5 6 Bag a = new Bag { Gold = 10 , Items = new List<string > { "钥匙" } }; Bag b = a; b.Gold = 20 ; b.Items.Add("药水" ); Console.WriteLine(a.Gold); Console.WriteLine(a.Items.Count);
复制结构体会逐字段复制,引用字段只复制引用,不会深拷贝对象。default(Bag) 中 Items 为 null,即使字段被声明为非可空引用,也要考虑结构体默认初始化。可使用不可变结构体和构造验证降低风险。
12.4 类中的值和引用 类对象里可同时有值字段与引用字段;赋值类变量只复制类对象引用,因此通过两个变量看到的是同一份字段。
1 2 3 4 5 public sealed class Wallet { public int Gold; public List<string > Items = new List<string >(); }
1 2 3 4 Wallet a = new Wallet { Gold = 10 }; Wallet b = a; b.Gold = 20 ; Console.WriteLine(a.Gold);
MemberwiseClone() 创建浅复制:新外层对象有独立值字段,但引用字段仍共享目标。深复制需要按业务关系处理整个对象图,尤其要考虑循环和共享关系,不能简单写个通用递归就认为正确。
12.5 数组中的存储规则
数组
槽位内容
新建后默认内容
Clone 结果
int[]
int 值
0
独立整数槽位
Bag[]
Bag 值,含其引用字段
Gold=0、Items=null
结构体复制,Items 仍可能共享
Wallet[]
Wallet 引用
null
新数组仍引用相同 Wallet
int[][]
每行 int[] 的引用
null
Clone 只复制外层行引用
数组本身始终是引用类型。new Wallet[3] 只创建三个引用槽位,没有创建三个 Wallet。对 List 索引器返回的结构体不能直接改字段:先取出局部副本,修改后写回;数组元素变量的规则不同,可直接修改其结构体字段。结构体属性返回值也容易出现“改了副本”的问题。
12.6 结构体实现接口与装箱 1 2 3 4 5 6 7 public interface ICounter { void Add () ; }public struct Counter : ICounter { public int Value; public void Add () => Value++; }public static void AddOne <T >(ref T value ) where T : struct , ICounter => value .Add();
1 2 3 4 5 6 7 Counter counter = new Counter(); ICounter boxed = counter; boxed.Add(); Console.WriteLine(counter.Value); Console.WriteLine(((Counter)boxed).Value); AddOne(ref counter); Console.WriteLine(counter.Value);
结构体可实现接口,但不能继承自定义类或另一个结构体;它隐式继承 System.ValueType。泛型约束调用通常可避免转换成接口导致的装箱;这里使用 ref 还保证修改原结构体,而不是参数副本。并非所有泛型代码永远不装箱,应根据具体转换和调用分析。
第十三章:项目实践——俄罗斯方块 13.1 项目目标与规则 本项目把前面教程的控制台、数组、循环、结构体、接口、封装继承多态,与本教程的泛型、事件、线程、反射和迭代器连起来。先读规则,再读模型,再读主循环,不要从绘制细节开始。
固定棋盘为 10 列 × 20 行,七种四格方块 I/O/T/S/Z/J/L。
七袋随机:每袋恰有七种方块各一个,用 Fisher–Yates 洗牌后顺序出队。
方块自动下落,左右移动、顺时针旋转、软降、硬降。下落失败立即锁定。
一行全部填满时删除,上方行向下压缩;一次可消 1~4 行。
新方块出生即发生碰撞时结束;开始、游戏、结束分别由场景类管理。
软降每成功移动一格 +1,硬降每格 +2;消 1/2/3/4 行基础分为 100/300/500/800,再乘消行前等级。
每累计 10 行升一级;下落间隔从 600ms 起每级减 45ms,最短 80ms。
P 暂停,Esc 回菜单,结束后 R 重开,Enter 回菜单,Esc 退出。
旋转是教学简化规则:在 4×4 模板内旋转,O 不变,依次试水平偏移 0、-1、+1、-2、+2;没有完整 SRS、向上踢墙、隐藏出生行、锁定延迟、Hold、T-Spin 或连击规则。读者应将其视为完整可玩的教学版本,而非竞技规则复刻。
界面 72 列 × 32 行,上方约四分之三为棋盘、下一块预览与状态,下方为最近五条落地/消行日志。游戏没有战斗,因此日志记录消行和操作反馈。活动方块显示 [],固定方块显示字母;边框始终绘制。窗口过小时暂停更新并显示尺寸提示,恢复后不会追赶整段停顿时间。
13.2 文件夹与职责
目录/文件
职责
本章应用
Program.cs
组装依赖、控制台入口、资源边界
using、异常处理
Application/GameApplication.cs
主循环、场景切换
接口依赖、多态
Contracts/
输入、时钟、渲染、工厂、只读视图协议
泛型协变、接口隔离
Domain/GridPoint.cs
坐标、方块枚举、格子值
只读结构体、运算符重载
Domain/Board.cs
已锁定格子、碰撞、消行
可空二维数组、泛型迭代器
Domain/PieceCatalog.cs
模板、旋转缓存、活动方块
Dictionary、封装、yield
Domain/StatisticsSnapshot.cs
不可变统计数据视图与排名
List、委托排序
Definitions/StandardPieces.cs
七种方块的初始形状
抽象类、继承、多态
Metadata/
特性、反射工厂
Type、Assembly、Activator、约束
Core/GameEngine.cs
移动、旋转、落地、计分
事件、规则封装
Core/RoundSession.cs
每局对象与订阅生命周期
IDisposable、事件取消
Core/RoundFactory.cs
创建每局新依赖
IFactory
Scenes/
开始、游戏、结束场景
Scene 抽象类、重写
Infrastructure/
真实输入、七袋随机、时钟、控制台恢复
Queue、Random、平台适配
Services/StatisticsWorker.cs
后台收集落地统计
Thread、lock、Queue、信号
Presentation/
菜单与游戏绘制
分部类、StringBuilder、嵌套 Canvas
Diagnostics/GameTrace.cs
调试日志
Conditional、调用者信息
HighExamples/
章节示例和规则检查
单链表、委托、装箱、自动验证
项目不强行把所有旧集合塞进游戏。生产路径采用类型安全的泛型集合;ArrayList 等在章节示例中演示。单向链表与标准迭代器也保留在独立示例中,不为展示数据结构而增加游戏复杂度。
flowchart TD
App["GameApplication"] --> Scene["Scene:菜单 / 游戏 / 结束"]
Scene --> Engine["GameEngine:规则"]
Scene --> Render["IRenderer:绘制"]
Engine --> Board["Board + PieceCatalog"]
Engine --> Stats["PieceLocked → StatisticsWorker"]
13.3 坐标与数据模型 坐标原点在棋盘左上,x 向右,y 向下。cells[y,x] 的第一维是行,与索引器 board[x,y] 参数顺序不同,必须明确。空格用 TetrominoKind? 的 null 表示;enum 的 0 是 I,不应拿 0 当空格。
GridPoint 是只读结构体,提供值相等、GetHashCode、加法和 ==,可用于 HashSet 去重。Board 内部二维数组不向外暴露,渲染层只能通过 IGameView 取得只读状态和枚举序列。
活动方块由 PieceDescriptor、Position、Rotation 组成。移动和旋转先产生候选对象,通过碰撞检查后才替换 active,失败时原方块保持原状。这是“先验证,再提交”的小型状态更新方式,避免移动一半才发现重叠。
旋转公式是 (x,y) → (3-y,x)。四次旋转应回到原始格子集合;O 特判不变。这个公式围绕模板中心旋转,故 I、J 等的视觉位置会与完整 SRS 不同。
13.4 碰撞、下落与锁定 Board.CanPlace 对四个格子逐个检查边界和是否被占据,必须先判断边界,再读取数组,利用短路避免越界。
1 2 3 foreach (GridPoint p in piece.Cells) if (!Contains(p) || cells[p.Y, p.X].HasValue) return false ;return true ;
DropOne 尝试向下移动,成功可奖励软降分;失败调用 LockActive。HardDrop 反复 TryMove 到最后合法位置,再统一锁定一次。结束后所有移动返回或无效果,避免重复落地、重复加分。
LockActive 的顺序是:写入棋盘 → 消行 → 按旧等级计算奖励 → 更新行数和分数 → 生成下一块并检查结束 → 发布落地事件。事件处理器能观察到完整的新状态;外部事件处理器不应直接改动引擎内部状态。
13.5 消行压缩算法 使用 readRow 与 writeRow 从底部向上扫描:满行跳过,不满行复制到 writeRow,再让 writeRow 上移。最后把顶部剩余行清空。单次消行成本 O(行数×列数),不需要反复移动整个棋盘。
1 2 3 4 5 6 7 8 9 10 11 12 int writeRow = Rows - 1 ;for (int readRow = Rows - 1 ; readRow >= 0 ; readRow--) { bool full = true ; for (int x = 0 ; x < Columns; x++) if (!cells[readRow, x].HasValue) { full = false ; break ; } if (full) continue ; for (int x = 0 ; x < Columns; x++) cells[writeRow, x] = cells[readRow, x]; writeRow--; }for (; writeRow >= 0 ; writeRow--) for (int x = 0 ; x < Columns; x++) cells[writeRow, x] = null ;
这段解释版省略 cleared 计数,源码保留完整返回值。连续满行和中间隔着非满行都应正确;从上往下逐行删除容易漏行,因此配套检查覆盖所有四行满/不满组合。
13.6 主循环与场景生命周期 主循环每轮:检查窗口 → 读取有限数量按键 → 计算经过时间 → 更新场景 → 绘制 → 小幅休眠。时间用 Stopwatch,不能用“每循环一次就下移一格”,否则不同机器速度不同。
场景有 Enter、Handle、Update、Render、Exit。开始场景只处理开始/退出;游戏场景控制暂停和落地日志;结束场景展示得分、行数、方块排名。切换时旧场景先 Exit,新场景再 Enter,订阅和清理均有对称位置。
PlayScene 累积时间,达到 FallMilliseconds 就 Tick;暂停时不积累时间,恢复时不瞬间下落多格。硬降后重置下落计时,避免新方块刚生成就吃掉旧方块剩余的时间。
13.7 事件与统计线程 PieceLockedEventArgs 仅保存枚举和整数,只读属性使数据交给后台后不再变化。StatisticsWorker 在 lock 内入队并发信号;后台从队列取出事件,用 Dictionary 更新计数;Capture 在锁内复制快照。
AutoResetEvent 的信号可能合并,所以它只表示“可能有工作”,事件本身必须留在 Queue 中。消费者醒来后持续处理,队空才再等待。不能把信号次数当成事件次数。
退出顺序:取消游戏事件订阅 → 标记 stopping 并发信号 → 不持锁 Join → 工作线程处理完已入队事件后返回 → 释放信号。结束场景读取 Join 之后的快照,保证最终计数不落后。正常游戏帧里的统计可能短暂落后于画面,这是异步统计的预期表现。
13.8 反射注册方块 每个方块定义实现 IPieceDefinition,并标记 [Piece(TetrominoKind.T,"T")]。启动时扫描明确指定的自身程序集,过滤非抽象类和接口实现,再读取特性、检查公开无参构造、Activator.CreateInstance,最后建立字典。
这是本教程的“泛型 + 约束 + 反射 + 特性”的组合案例:CreateAll<TContract,TAttribute> 用 class 和 Attribute 约束保证基本能力;IsAssignableFrom 进行运行时筛选;注册完成后正常游戏走接口和缓存,不每帧反射。
新增方块还需同步枚举与规则假设;代码仅保证七种标准方块,并不承诺“随便增加任意形状就完全兼容”。结构扩展需要一并检查随机袋、棋盘、计分与渲染范围。
13.9 运行与发布 1 2 3 dotnet build TetrisGame/TetrisGame.csproj -c Release dotnet run --project TetrisGame/TetrisGame.csproj -c Release dotnet run --project HighExamples/HighExamples.csproj -- --checks-only
框架依赖发布:
1 dotnet publish TetrisGame/TetrisGame.csproj -c Release -o publish
产物仍需要目标机器的兼容 .NET 运行时;自包含发布需要指定目标系统运行标识,不能把 Windows 产物直接当成所有系统通用程序。源码包不包含 SDK、bin、obj 或第三方组件。
13.10 自测流程与项目局限 先按 Enter 开始,左右移动至边界并旋转;按 P 后观察方块停住,等待几秒再恢复;按 Space 多次堆叠到出生区触发结束;按 R 确认分数、棋盘和统计都重置;Esc 回菜单再退出。缩小终端后应出现提示,调大后继续。
自动检查用固定随机种子覆盖七袋预览与出队、四次旋转、边界、棋盘复制隔离、消行顺序、精确计分、出生碰撞、游戏结束后无额外操作、暂停及统计清空。随机模拟用于发现状态不变量问题,不能替代所有人工可玩性评估。
Console API 在不同终端上的按键重复速度与刷新效果可能不同;动态调整窗口恰逢绘制时程序会提示重新启动。反射扫描依赖元数据,本文未启用裁剪或 Native AOT;若以后启用,需为反射目标显式保留元数据或改为生成注册表。
练习与参考答案
章节
练习
参考答案要点
1
为什么不能把装箱 int 直接拆成 long?
拆箱必须匹配盒中值类型,先拆 int 再数值转换
2
编写能创建对象且调用 Id 的工厂
where T : IHasId, new();接口提供 Id,new() 提供构造能力
3
删除单链表最后一个节点要更新什么?
前驱 Next=null、tail=前驱;唯一节点时 head/tail 均 null;Count/version 更新
4
为什么事件处理器要取消订阅?
发布者可能持有订阅者引用,导致生命周期延长或旧场景重复响应
5
给同分玩家一个确定顺序
比分数,再用姓名或唯一 id;不用减法比较整数
6
为什么 List 不能给 List?
Animal 列表允许加 Cat,会破坏 Dog 列表类型安全;只读生产接口可协变
7
为什么 Sleep 不能保证工作已完成?
调度时间不确定,也没有完成状态或内存同步协议;用 Join/信号/Task
8
Release 中被 Conditional 省略的日志参数会执行吗?
未保留调用时通常不会,因此不能依赖参数副作用
9
特性为何不会自动调用业务代码?
它通常只保存元数据,需要消费者读取解释
10
yield 方法中的异常为什么可能晚发生?
方法体通过状态机在 MoveNext 时执行;可拆验证与迭代主体
11
var 与 dynamic 的区别?
var 编译期静态推断,dynamic 把部分绑定延迟到运行时
12
复制含 List 字段的结构体为何会共享内容?
结构体字段复制,List 字段复制的是引用
项目
为什么需要在 Join 后读取结束统计?
后台可能尚未消费完队列,Join 确认退出并完成积压工作
建议扩展:增加按键设置、最高分持久化、幽灵落点、完整 SRS、Hold。每次只新增一项规则,并先为状态边界写检查。若新增存档线程,继续保持棋盘由主线程独占,把不可变快照交给后台。
官方资料与阅读索引 以下为 Microsoft Learn 官方资料,查阅于 2026-10-28。本文解释与示例以固定的 C# 12 语法为主,API 以配套目标框架为准;页面可能包含更新版本内容。
验证说明 本次交付中,游戏 Debug/Release 与章节示例通过编译,游戏构建零警告零错误。规则程序通过 8,589 项断言;正文 57 段 C# 代码在补充所需 using、类型声明或方法体上下文后编译通过,var 示例有两条未使用变量警告。终端按键测试跑通开始、暂停、恢复、结束、重开、菜单和退出。源码包的 verification/验证记录.md 说明检查范围与限制,Windows 原生 DLL 示例仅做编译验证。
附录:完整俄罗斯方块源码 以下按相对路径列出游戏全部源码与项目文件。可直接使用压缩包;手动重建时按路径保存。HighExamples 的完整章节示例、单链表和规则检查位于源码包中。
TetrisGame/Application/GameApplication.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 using System;using TetrisGame.Contracts;using TetrisGame.Scenes;namespace TetrisGame.Application { public sealed class GameApplication : ISceneNavigator { private readonly IInputSource input; private readonly IRenderer renderer; private readonly IGameClock clock; private readonly IViewport viewport; private readonly IFactory<TetrisGame.Core.RoundSession> factory; private Scene scene; private bool running = true ; public GameApplication (IInputSource input, IRenderer renderer, IGameClock clock, IViewport viewport, IFactory<TetrisGame.Core.RoundSession> factory ) { this .input = input ?? throw new ArgumentNullException(nameof (input)); this .renderer = renderer ?? throw new ArgumentNullException(nameof (renderer)); this .clock = clock ?? throw new ArgumentNullException(nameof (clock)); this .viewport = viewport ?? throw new ArgumentNullException(nameof (viewport)); this .factory = factory ?? throw new ArgumentNullException(nameof (factory)); scene = new StartScene(this , renderer); } public void Run () { scene.Enter(); try { TimeSpan last = clock.Elapsed; while (running) { if (!viewport.IsReady) { viewport.ShowResizeHint(); if (input.TryRead(out InputCommand smallWindowCommand) && smallWindowCommand == InputCommand.Escape) Quit(); input.ClearPending(); clock.Sleep(50 ); last = clock.Elapsed; continue ; } for (int count = 0 ; count < 8 && input.TryRead(out InputCommand command); count++) { Scene previous = scene; scene.Handle(command); if (!running || !ReferenceEquals(previous, scene)) { input.ClearPending(); last = clock.Elapsed; break ; } } if (!running) break ; TimeSpan now = clock.Elapsed; TimeSpan elapsed = now - last; last = now; if (elapsed > TimeSpan.FromMilliseconds(250 )) elapsed = TimeSpan.FromMilliseconds(250 ); scene.Update(elapsed); scene.Render(); clock.Sleep(16 ); } } finally { scene.Exit(); } } private void ChangeScene (Scene next ) { scene.Exit(); scene = next; scene.Enter(); } public void StartRound () => ChangeScene(new PlayScene(this , renderer, factory.Create())); public void ShowMenu () => ChangeScene(new StartScene(this , renderer)); public void EndRound (TetrisGame.Core.RoundSummary result ) => ChangeScene(new EndScene(this , renderer, result)); public void Quit () => running = false ; } }
TetrisGame/Contracts/IGameView.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 using System.Collections.Generic;using TetrisGame.Domain;namespace TetrisGame.Contracts { public interface IGameView { int Columns { get ; } int Rows { get ; } int Score { get ; } int Lines { get ; } int Level { get ; } bool IsGameOver { get ; } TetrominoKind NextKind { get ; } IEnumerable<BoardCell> LockedCells { get ; } IEnumerable<GridPoint> ActiveCells { get ; } } }
TetrisGame/Contracts/PieceContracts.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using System.Collections.Generic;using TetrisGame.Domain;namespace TetrisGame.Contracts { public interface IPieceDefinition { TetrominoKind Kind { get ; } GridPoint[] CreateCells () ; } public interface IPieceSource { TetrominoKind Next () ; IReadOnlyList<TetrominoKind> Preview (int count ) ; } public interface IFactory <out T > { T Create () ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using System;using System.Collections.Generic;using TetrisGame.Core;using TetrisGame.Domain;namespace TetrisGame.Contracts { public enum InputCommand { None, Start, Left, Right, Rotate, SoftDrop, HardDrop, Pause, Replay, Escape } public interface IInputSource { bool TryRead (out InputCommand command ) ; void ClearPending () ; } public interface IGameClock { TimeSpan Elapsed { get ; } void Sleep (int milliseconds ) ; } public interface IViewport { bool IsReady { get ; } void ShowResizeHint () ; } public interface IMenuRenderer { void DrawStart () ; void DrawEnd (RoundSummary result ) ; } public interface IGameRenderer { void DrawGame (IGameView game, bool paused, IReadOnlyList<string > messages, StatisticsSnapshot stats ) ; } public interface IRenderer : IMenuRenderer , IGameRenderer { } public interface ISceneNavigator { void StartRound () ; void ShowMenu () ; void EndRound (RoundSummary summary ) ; void Quit () ; } }
TetrisGame/Core/GameEngine.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 using System;using System.Collections.Generic;using TetrisGame.Contracts;using TetrisGame.Domain;namespace TetrisGame.Core { public sealed class PieceLockedEventArgs : EventArgs { public TetrominoKind Kind { get ; } public int ClearedLines { get ; } public int AwardedPoints { get ; } public PieceLockedEventArgs (TetrominoKind kind, int lines, int points ) { Kind = kind; ClearedLines = lines; AwardedPoints = points; } } public sealed class GameEngine : IGameView { private readonly Board board; private readonly PieceCatalog catalog; private readonly IPieceSource source; private FallingPiece active; public event EventHandler<PieceLockedEventArgs>? PieceLocked; public int Columns => board.Columns; public int Rows => board.Rows; public int Score { get ; private set ; } public int Lines { get ; private set ; } public int Level => 1 + Lines / 10 ; public int FallMilliseconds => Math.Max(80 , 600 - (Level - 1 ) * 45 ); public bool IsGameOver { get ; private set ; } public TetrominoKind NextKind => source.Preview(1 )[0 ]; public IEnumerable<GridPoint> ActiveCells => active.Cells; public IEnumerable<BoardCell> LockedCells { get { foreach (BoardCell cell in board) yield return cell; } } public GameEngine (Board board, PieceCatalog catalog, IPieceSource source ) { ArgumentNullException.ThrowIfNull(board); this .board = new Board(board.Columns, board.Rows, board); this .catalog = catalog ?? throw new ArgumentNullException(nameof (catalog)); this .source = source ?? throw new ArgumentNullException(nameof (source)); active = Spawn(); } private FallingPiece Spawn () { FallingPiece piece = new FallingPiece(catalog[source.Next()], new GridPoint(Columns / 2 - 2 , 0 )); if (!board.CanPlace(piece)) IsGameOver = true ; return piece; } public bool MoveHorizontal (int dx ) { if (dx != -1 && dx != 1 ) throw new ArgumentOutOfRangeException(nameof (dx)); return TryMove(dx, 0 ); } private bool TryMove (int dx, int dy ) { if (IsGameOver) return false ; FallingPiece candidate = active.Move(dx, dy); if (!board.CanPlace(candidate)) return false ; active = candidate; return true ; } public bool Rotate () { if (IsGameOver) return false ; FallingPiece rotated = active.Rotate(); foreach (int dx in new int [] { 0 , -1 , 1 , -2 , 2 }) { FallingPiece candidate = rotated.Move(dx, 0 ); if (!board.CanPlace(candidate)) continue ; active = candidate; return true ; } return false ; } public void Tick () => DropOne(awardPoint: false ); public void SoftDrop () => DropOne(awardPoint: true ); private void DropOne (bool awardPoint ) { if (IsGameOver) return ; if (TryMove(0 , 1 )) { if (awardPoint) Score++; } else LockActive(); } public void HardDrop () { if (IsGameOver) return ; int distance = 0 ; while (TryMove(0 , 1 )) distance++; Score += distance * 2 ; LockActive(); } private void LockActive () { TetrominoKind kind = active.Kind; board.Lock(active); int cleared = board.ClearFullRows(); int basePoints = cleared switch { 0 => 0 , 1 => 100 , 2 => 300 , 3 => 500 , 4 => 800 , _ => throw new InvalidOperationException("Too many full rows." ) }; int points = basePoints * Level; Lines += cleared; Score += points; active = Spawn(); PieceLocked?.Invoke(this , new PieceLockedEventArgs(kind, cleared, points)); } } }
TetrisGame/Core/RoundFactory.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 using System;using TetrisGame.Contracts;using TetrisGame.Domain;using TetrisGame.Infrastructure;namespace TetrisGame.Core { public sealed class RoundFactory : IFactory <RoundSession > { private readonly PieceCatalog catalog; public RoundFactory (PieceCatalog catalog ) => this .catalog = catalog; public RoundSession Create () => new RoundSession(new GameEngine(new Board(10 , 20 ), catalog, new SevenBagSource(new Random()))); } }
TetrisGame/Core/RoundSession.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 using System;using TetrisGame.Domain;using TetrisGame.Services;namespace TetrisGame.Core { public sealed class RoundSession : IDisposable { private bool disposed; public GameEngine Game { get ; } public StatisticsWorker Statistics { get ; } public RoundSession (GameEngine game ) { Game = game ?? throw new ArgumentNullException(nameof (game)); Statistics = new StatisticsWorker(); Game.PieceLocked += Statistics.OnPieceLocked; } public void Dispose () { if (disposed) return ; disposed = true ; Game.PieceLocked -= Statistics.OnPieceLocked; Statistics.Dispose(); } } public sealed class RoundSummary { public int Score { get ; } public int Lines { get ; } public int Level { get ; } public StatisticsSnapshot Statistics { get ; } public RoundSummary (RoundSession session ) { session.Dispose(); Score = session.Game.Score; Lines = session.Game.Lines; Level = session.Game.Level; Statistics = session.Statistics.Capture(); } } }
TetrisGame/Definitions/StandardPieces.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using System.Collections.Generic;using TetrisGame.Contracts;using TetrisGame.Domain;using TetrisGame.Metadata;namespace TetrisGame.Definitions { public abstract class StandardPiece : IPieceDefinition { public abstract TetrominoKind Kind { get ; } public abstract GridPoint[] CreateCells () ; protected static GridPoint[] Parse (params string [] rows ) { List<GridPoint> cells = new List<GridPoint>(); for (int y = 0 ; y < rows.Length; y++) for (int x = 0 ; x < rows[y].Length; x++) if (rows[y][x] == '#' ) cells.Add(new GridPoint(x, y)); return cells.ToArray(); } } [Piece(TetrominoKind.I, "I" ) ] public sealed class IPiece : StandardPiece { public override TetrominoKind Kind => TetrominoKind.I; public override GridPoint[] CreateCells () => Parse("...." , "####" , "...." , "...." ); } [Piece(TetrominoKind.O, "O" ) ] public sealed class OPiece : StandardPiece { public override TetrominoKind Kind => TetrominoKind.O; public override GridPoint[] CreateCells () => Parse(".##." , ".##." , "...." , "...." ); } [Piece(TetrominoKind.T, "T" ) ] public sealed class TPiece : StandardPiece { public override TetrominoKind Kind => TetrominoKind.T; public override GridPoint[] CreateCells () => Parse(".#.." , "###." , "...." , "...." ); } [Piece(TetrominoKind.S, "S" ) ] public sealed class SPiece : StandardPiece { public override TetrominoKind Kind => TetrominoKind.S; public override GridPoint[] CreateCells () => Parse(".##." , "##.." , "...." , "...." ); } [Piece(TetrominoKind.Z, "Z" ) ] public sealed class ZPiece : StandardPiece { public override TetrominoKind Kind => TetrominoKind.Z; public override GridPoint[] CreateCells () => Parse("##.." , ".##." , "...." , "...." ); } [Piece(TetrominoKind.J, "J" ) ] public sealed class JPiece : StandardPiece { public override TetrominoKind Kind => TetrominoKind.J; public override GridPoint[] CreateCells () => Parse("#..." , "###." , "...." , "...." ); } [Piece(TetrominoKind.L, "L" ) ] public sealed class LPiece : StandardPiece { public override TetrominoKind Kind => TetrominoKind.L; public override GridPoint[] CreateCells () => Parse("..#." , "###." , "...." , "...." ); } }
TetrisGame/Diagnostics/GameTrace.cs 1 2 3 4 5 6 7 8 9 10 11 12 using System.Diagnostics;using System.Runtime.CompilerServices;namespace TetrisGame.Diagnostics { public static class GameTrace { [Conditional("DEBUG" ) ] public static void Write (string message, [CallerMemberName] string member = "" , [CallerLineNumber] int line = 0 ) => Debug.WriteLine($"{member} :{line} {message} " ); } }
TetrisGame/Domain/Board.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 using System;using System.Collections;using System.Collections.Generic;namespace TetrisGame.Domain { public sealed class Board : IEnumerable <BoardCell > { private readonly TetrominoKind?[,] cells; public int Columns { get ; } public int Rows { get ; } public Board (int columns, int rows, IEnumerable<BoardCell>? initial = null ) { if (columns < 4 || rows < 4 ) throw new ArgumentOutOfRangeException(nameof (columns)); Columns = columns; Rows = rows; cells = new TetrominoKind?[rows, columns]; if (initial is not null ) foreach (BoardCell cell in initial) { GridPoint p = cell.Position; if (!Contains(p) || cells[p.Y, p.X].HasValue) throw new ArgumentException("Invalid initial cell." , nameof (initial)); cells[p.Y, p.X] = cell.Kind; } } public TetrominoKind? this [int x, int y] => cells[y, x]; public bool Contains (GridPoint p ) => p.X >= 0 && p.X < Columns && p.Y >= 0 && p.Y < Rows; public bool CanPlace (FallingPiece piece ) { foreach (GridPoint p in piece.Cells) if (!Contains(p) || cells[p.Y, p.X].HasValue) return false ; return true ; } public void Lock (FallingPiece piece ) { if (!CanPlace(piece)) throw new InvalidOperationException("Cannot lock overlapping or out-of-board cells." ); foreach (GridPoint p in piece.Cells) cells[p.Y, p.X] = piece.Kind; } public int ClearFullRows () { int writeRow = Rows - 1 , cleared = 0 ; for (int readRow = Rows - 1 ; readRow >= 0 ; readRow--) { bool full = true ; for (int x = 0 ; x < Columns; x++) if (!cells[readRow, x].HasValue) { full = false ; break ; } if (full) { cleared++; continue ; } for (int x = 0 ; x < Columns; x++) cells[writeRow, x] = cells[readRow, x]; writeRow--; } for (; writeRow >= 0 ; writeRow--) for (int x = 0 ; x < Columns; x++) cells[writeRow, x] = null ; return cleared; } public IEnumerator<BoardCell> GetEnumerator () { for (int y = 0 ; y < Rows; y++) for (int x = 0 ; x < Columns; x++) if (cells[y, x] is TetrominoKind kind) yield return new BoardCell (new GridPoint(x, y ), kind) ; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } }
TetrisGame/Domain/GridPoint.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 using System;namespace TetrisGame.Domain { public readonly struct GridPoint : IEquatable<GridPoint> { public int X { get ; } public int Y { get ; } public GridPoint (int x, int y ) { X = x; Y = y; } public static GridPoint operator +(GridPoint a, GridPoint b) => new GridPoint(a.X + b.X, a.Y + b.Y); public bool Equals (GridPoint other ) => X == other.X && Y == other.Y; public override bool Equals (object ? value ) => value is GridPoint other && Equals(other); public override int GetHashCode () => HashCode.Combine(X, Y); public static bool operator ==(GridPoint a, GridPoint b) => a.Equals(b); public static bool operator !=(GridPoint a, GridPoint b) => !a.Equals(b); public override string ToString () => $"({X} , {Y} )" ; } public enum TetrominoKind { I, O, T, S, Z, J, L } public readonly struct BoardCell { public GridPoint Position { get ; } public TetrominoKind Kind { get ; } public BoardCell (GridPoint position, TetrominoKind kind ) { Position = position; Kind = kind; } } }
TetrisGame/Domain/PieceCatalog.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 using System;using System.Collections.Generic;using TetrisGame.Contracts;using TetrisGame.Metadata;namespace TetrisGame.Domain { public sealed class PieceDescriptor { private readonly GridPoint[][] rotations = new GridPoint[4 ][]; public TetrominoKind Kind { get ; } public string Name { get ; } public PieceDescriptor (TetrominoKind kind, string name, GridPoint[] cells ) { ArgumentNullException.ThrowIfNull(cells); if (cells.Length != 4 ) throw new ArgumentException("A tetromino needs four cells." , nameof (cells)); for (int i = 0 ; i < cells.Length; i++) { if (cells[i].X < 0 || cells[i].X > 3 || cells[i].Y < 0 || cells[i].Y > 3 ) throw new ArgumentException("Cells must fit in a 4 x 4 template." , nameof (cells)); for (int j = 0 ; j < i; j++) if (cells[i] == cells[j]) throw new ArgumentException("Duplicate template cell." , nameof (cells)); } Kind = kind; Name = name; rotations[0 ] = (GridPoint[])cells.Clone(); for (int turn = 1 ; turn < 4 ; turn++) { rotations[turn] = new GridPoint[4 ]; for (int i = 0 ; i < 4 ; i++) { GridPoint previous = rotations[turn - 1 ][i]; rotations[turn][i] = kind == TetrominoKind.O ? previous : new GridPoint(3 - previous.Y, previous.X); } } } public IEnumerable<GridPoint> Offsets (int rotation ) { if (rotation < 0 || rotation > 3 ) throw new ArgumentOutOfRangeException(nameof (rotation)); foreach (GridPoint point in rotations[rotation]) yield return point; } } public sealed class PieceCatalog { private readonly Dictionary<TetrominoKind, PieceDescriptor> definitions = new Dictionary<TetrominoKind, PieceDescriptor>(); public PieceDescriptor this [TetrominoKind kind] => definitions[kind]; public int Count => definitions.Count; public PieceCatalog () { foreach (Registration<IPieceDefinition, PieceAttribute> item in ReflectionRegistry.CreateAll<IPieceDefinition, PieceAttribute>(typeof (PieceCatalog).Assembly)) { if (item.Instance.Kind != item.Metadata.Kind) throw new InvalidOperationException("Piece metadata mismatch." ); definitions.Add(item.Metadata.Kind, new PieceDescriptor(item.Metadata.Kind, item.Metadata.Name, item.Instance.CreateCells())); } foreach (TetrominoKind kind in Enum.GetValues<TetrominoKind>()) if (!definitions.ContainsKey(kind)) throw new InvalidOperationException($"Missing piece {kind} ." ); } } public sealed class FallingPiece { private readonly PieceDescriptor descriptor; public TetrominoKind Kind => descriptor.Kind; public GridPoint Position { get ; } public int Rotation { get ; } public FallingPiece (PieceDescriptor descriptor, GridPoint position, int rotation = 0 ) { this .descriptor = descriptor ?? throw new ArgumentNullException(nameof (descriptor)); if (rotation < 0 || rotation > 3 ) throw new ArgumentOutOfRangeException(nameof (rotation)); Position = position; Rotation = rotation; } public IEnumerable<GridPoint> Cells { get { foreach (GridPoint offset in descriptor.Offsets(Rotation)) yield return Position + offset; } } public FallingPiece Move (int dx, int dy ) => new FallingPiece(descriptor, Position + new GridPoint(dx, dy), Rotation); public FallingPiece Rotate () => new FallingPiece(descriptor, Position, (Rotation + 1 ) % 4 ); } }
TetrisGame/Domain/StatisticsSnapshot.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 using System;using System.Collections.Generic;namespace TetrisGame.Domain { public readonly struct PieceCount { public TetrominoKind Kind { get ; } public int Count { get ; } public PieceCount (TetrominoKind kind, int count ) { Kind = kind; Count = count; } } public sealed class StatisticsSnapshot { private readonly List<PieceCount> counts; public long Pieces { get ; } public long Lines { get ; } public long ClearPoints { get ; } public StatisticsSnapshot (long pieces, long lines, long points, IEnumerable<PieceCount> counts ) { Pieces = pieces; Lines = lines; ClearPoints = points; this .counts = new List<PieceCount>(counts); } public IReadOnlyList<PieceCount> GetRanking () { List<PieceCount> ranking = new List<PieceCount>(counts); ranking.Sort((a, b) => { int byCount = b.Count.CompareTo(a.Count); return byCount != 0 ? byCount : a.Kind.CompareTo(b.Kind); }); return ranking.AsReadOnly(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 using System;using TetrisGame.Contracts;namespace TetrisGame.Infrastructure { public sealed class ConsoleInputSource : IInputSource { public bool TryRead (out InputCommand command ) { command = InputCommand.None; if (!Console.KeyAvailable) return false ; switch (Console.ReadKey(true ).Key) { case ConsoleKey.Enter: command = InputCommand.Start; break ; case ConsoleKey.A: case ConsoleKey.LeftArrow: command = InputCommand.Left; break ; case ConsoleKey.D: case ConsoleKey.RightArrow: command = InputCommand.Right; break ; case ConsoleKey.W: case ConsoleKey.UpArrow: command = InputCommand.Rotate; break ; case ConsoleKey.S: case ConsoleKey.DownArrow: command = InputCommand.SoftDrop; break ; case ConsoleKey.Spacebar: command = InputCommand.HardDrop; break ; case ConsoleKey.P: command = InputCommand.Pause; break ; case ConsoleKey.R: command = InputCommand.Replay; break ; case ConsoleKey.Escape: command = InputCommand.Escape; break ; } return true ; } public void ClearPending () { for (int i = 0 ; i < 64 && Console.KeyAvailable; i++) Console.ReadKey(true ); } } }
TetrisGame/Infrastructure/ConsoleSession.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 using System;namespace TetrisGame.Infrastructure { public sealed class ConsoleSession : IDisposable { private bool disposed; public ConsoleSession () { try { Console.CursorVisible = false ; Console.Clear(); } catch { Console.CursorVisible = true ; throw ; } } public void Dispose () { if (disposed) return ; disposed = true ; Console.CursorVisible = true ; Console.ResetColor(); Console.WriteLine(); } } }
TetrisGame/Infrastructure/SevenBagSource.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 using System;using System.Collections.Generic;using TetrisGame.Contracts;using TetrisGame.Domain;namespace TetrisGame.Infrastructure { public sealed class SevenBagSource : IPieceSource { private readonly Queue<TetrominoKind> queue = new Queue<TetrominoKind>(); private readonly Random random; public SevenBagSource (Random random ) => this .random = random ?? throw new ArgumentNullException(nameof (random)); private void Ensure (int count ) { while (queue.Count < count) { TetrominoKind[] bag = Enum.GetValues<TetrominoKind>(); for (int i = bag.Length - 1 ; i > 0 ; i--) { int j = random.Next(i + 1 ); (bag[i], bag[j]) = (bag[j], bag[i]); } foreach (TetrominoKind kind in bag) queue.Enqueue(kind); } } public TetrominoKind Next () { Ensure(1 ); return queue.Dequeue(); } public IReadOnlyList<TetrominoKind> Preview (int count ) { if (count < 0 ) throw new ArgumentOutOfRangeException(nameof (count)); Ensure(count); TetrominoKind[] result = new TetrominoKind[count]; int index = 0 ; foreach (TetrominoKind kind in queue) { if (index == count) break ; result[index++] = kind; } return Array.AsReadOnly(result); } } }
TetrisGame/Infrastructure/SystemGameClock.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 using System;using System.Diagnostics;using System.Threading;using TetrisGame.Contracts;namespace TetrisGame.Infrastructure { public sealed class SystemGameClock : IGameClock { private readonly Stopwatch stopwatch = Stopwatch.StartNew(); public TimeSpan Elapsed => stopwatch.Elapsed; public void Sleep (int milliseconds ) => Thread.Sleep(milliseconds); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 using System;using TetrisGame.Domain;namespace TetrisGame.Metadata { [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false) ] public sealed class PieceAttribute : Attribute { public TetrominoKind Kind { get ; } public string Name { get ; } public PieceAttribute (TetrominoKind kind, string name ) { Kind = kind; Name = name; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 using System;using System.Collections.Generic;using System.Reflection;namespace TetrisGame.Metadata { public sealed class Registration <TContract , TAttribute > where TContract : class where TAttribute : Attribute { public TContract Instance { get ; } public TAttribute Metadata { get ; } public Registration (TContract instance, TAttribute metadata ) { Instance = instance; Metadata = metadata; } } public static class ReflectionRegistry { public static IReadOnlyList <Registration <TContract , TAttribute >> CreateAll <TContract , TAttribute >(Assembly assembly ) where TContract : class where TAttribute : Attribute { ArgumentNullException.ThrowIfNull(assembly); List<Registration<TContract, TAttribute>> result = new List<Registration<TContract, TAttribute>>(); foreach (Type type in assembly.GetTypes()) { if (!type.IsClass || type.IsAbstract || type.ContainsGenericParameters || !typeof (TContract).IsAssignableFrom(type)) continue ; TAttribute? metadata = type.GetCustomAttribute<TAttribute>(inherit: false ); if (metadata is null ) continue ; if (type.GetConstructor(Type.EmptyTypes) is null ) throw new InvalidOperationException($"{type.Name} needs a public parameterless constructor." ); TContract instance = (TContract)(Activator.CreateInstance(type) ?? throw new InvalidOperationException("Constructor returned null." )); result.Add(new Registration<TContract, TAttribute>(instance, metadata)); } return result.AsReadOnly(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 using System.Text;using TetrisGame.Core;using TetrisGame.Domain;namespace TetrisGame.Presentation { public sealed partial class ConsoleRenderer { public void DrawStart () => Present(BuildStart()); public void DrawEnd (RoundSummary result ) => Present(BuildEnd(result)); private static Canvas BuildStart () { Canvas c = new Canvas(); c.Put(25 , 5 , "TETRIS / C# HIGH LEVEL" ); c.Put(6 , 9 , "Seven pieces. Clear complete rows to score." ); c.Put(6 , 12 , "A/D or arrows: move. W/Up: rotate. S/Down: soft drop." ); c.Put(6 , 15 , "SPACE: hard drop. P: pause/resume. ESC: menu." ); c.Put(6 , 18 , "Simple clockwise rotation with horizontal wall checks." ); c.Put(6 , 22 , "ENTER: start ESC: quit" ); return c; } private static Canvas BuildEnd (RoundSummary result ) { Canvas c = new Canvas(); c.Put(28 , 5 , "GAME OVER" ); c.Put(6 , 10 , $"Score: {result.Score} Lines: {result.Lines} Level: {result.Level} " ); c.Put(6 , 13 , $"Placed pieces: {result.Statistics.Pieces} " ); StringBuilder ranking = new StringBuilder("Most used: " ); foreach (PieceCount item in result.Statistics.GetRanking()) ranking.Append(item.Kind).Append('=' ).Append(item.Count).Append(' ' ); c.Put(6 , 16 , ranking.ToString()); c.Put(6 , 21 , "R: play again ENTER: main menu ESC: quit" ); return c; } } }
TetrisGame/Presentation/ConsoleRenderer.Play.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using System;using System.Collections.Generic;using TetrisGame.Contracts;using TetrisGame.Domain;namespace TetrisGame.Presentation { public sealed partial class ConsoleRenderer { private readonly PieceCatalog catalog; public ConsoleRenderer (PieceCatalog catalog ) => this .catalog = catalog; public void DrawGame (IGameView game, bool paused, IReadOnlyList<string > messages, StatisticsSnapshot stats ) => Present(BuildGame(game, paused, messages, stats)); private Canvas BuildGame (IGameView game, bool paused, IReadOnlyList<string > messages, StatisticsSnapshot stats ) { if (game.Columns > 10 || game.Rows > 20 ) throw new ArgumentException("Renderer supports up to 10 x 20 cells." ); Canvas c = new Canvas(); c.Line(2 ); c.Line(23 ); c.Line(24 ); c.Put(1 , 1 , $" TETRIS Score:{game.Score} Lines:{game.Lines} Level:{game.Level} {(paused ? "PAUSED" : "RUNNING" )} " ); c.Box(3 , 2 , game.Columns * 2 + 2 , game.Rows + 2 ); foreach (BoardCell cell in game.LockedCells) c.Put(4 + cell.Position.X * 2 , 3 + cell.Position.Y, cell.Kind.ToString() + " " ); foreach (GridPoint point in game.ActiveCells) if (point.X >= 0 && point.X < game.Columns && point.Y >= 0 && point.Y < game.Rows) c.Put(4 + point.X * 2 , 3 + point.Y, "[]" ); c.Put(30 , 4 , "NEXT: " + game.NextKind); foreach (GridPoint offset in catalog[game.NextKind].Offsets(0 )) c.Put(32 + offset.X * 2 , 6 + offset.Y, "[]" ); c.Put(30 , 10 , $"Placed: {stats.Pieces} " ); c.Put(30 , 12 , "A / D / arrows: move" ); c.Put(30 , 14 , "W / Up: rotate" ); c.Put(30 , 16 , "S / Down: soft drop" ); c.Put(30 , 18 , "SPACE: hard drop" ); c.Put(30 , 20 , "P: pause/resume" ); c.Put(30 , 21 , "ESC: main menu" ); c.Put(1 , 25 , " GAME LOG (latest 5)" ); int first = Math.Max(0 , messages.Count - 5 ); for (int i = first; i < messages.Count; i++) c.Put(1 , 26 + i - first, messages[i]); return c; } } }
TetrisGame/Presentation/ConsoleRenderer.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 using System;using System.Text;using TetrisGame.Contracts;namespace TetrisGame.Presentation { public sealed partial class ConsoleRenderer : IRenderer , IViewport { private const int Width = 72 ; private const int Height = 32 ; private bool resizeHintShown; public bool IsReady => Console.WindowWidth >= Width + 1 && Console.WindowHeight >= Height + 1 ; public void ShowResizeHint () { if (resizeHintShown) return ; resizeHintShown = true ; Console.Clear(); Console.WriteLine("Resize terminal to at least 73 columns x 33 rows." ); Console.WriteLine("The game waits automatically. ESC: quit." ); } private void Present (Canvas canvas ) { if (!IsReady) { ShowResizeHint(); return ; } resizeHintShown = false ; Console.SetCursorPosition(0 , 0 ); Console.Write(canvas.ToString()); } private sealed class Canvas { private readonly char [,] cells = new char [Height, Width]; public Canvas () { for (int y = 0 ; y < Height; y++) for (int x = 0 ; x < Width; x++) cells[y, x] = x == 0 || x == Width - 1 ? '|' : ' ' ; Line(0 ); Line(Height - 1 ); } public void Put (int x, int y, string text ) { for (int i = 0 ; i < text.Length && x + i < Width - 1 ; i++) cells[y, x + i] = text[i]; } public void Line (int y ) { for (int x = 0 ; x < Width; x++) cells[y, x] = x == 0 || x == Width - 1 ? '+' : '-' ; } public void Box (int left, int top, int width, int height ) { for (int y = top; y < top + height; y++) for (int x = left; x < left + width; x++) { bool horizontal = y == top || y == top + height - 1 ; bool vertical = x == left || x == left + width - 1 ; if (horizontal || vertical) cells[y, x] = horizontal && vertical ? '+' : horizontal ? '-' : '|' ; } } public override string ToString () { StringBuilder builder = new StringBuilder((Width + 2 ) * Height); for (int y = 0 ; y < Height; y++) { for (int x = 0 ; x < Width; x++) builder.Append(cells[y, x]); if (y < Height - 1 ) builder.AppendLine(); } return builder.ToString(); } } } }
TetrisGame/Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 using System;using System.IO;using TetrisGame.Application;using TetrisGame.Core;using TetrisGame.Diagnostics;using TetrisGame.Domain;using TetrisGame.Infrastructure;using TetrisGame.Presentation;namespace TetrisGame { internal static class Program { private static int Main () { if (Console.IsInputRedirected || Console.IsOutputRedirected) { Console.Error.WriteLine("Run this game in an interactive terminal." ); return 2 ; } try { PieceCatalog catalog = new PieceCatalog(); GameTrace.Write($"Loaded {catalog.Count} piece definitions." ); using (ConsoleSession session = new ConsoleSession()) { ConsoleRenderer renderer = new ConsoleRenderer(catalog); GameApplication app = new GameApplication(new ConsoleInputSource(), renderer, new SystemGameClock(), renderer, new RoundFactory(catalog)); app.Run(); } return 0 ; } catch (IOException ex) { Console.Error.WriteLine("Terminal I/O error: " + ex.Message); return 1 ; } catch (ArgumentOutOfRangeException) { Console.Error.WriteLine("Terminal resized during redraw. Resize and restart." ); return 1 ; } } } }
TetrisGame/Scenes/PlayScene.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 using System;using System.Collections.Generic;using TetrisGame.Contracts;using TetrisGame.Core;namespace TetrisGame.Scenes { public sealed class PlayScene : Scene { private readonly RoundSession session; private readonly IGameRenderer renderer; private readonly List<string > messages = new List<string >(); private TimeSpan accumulated; public bool Paused { get ; private set ; } public PlayScene (ISceneNavigator navigator, IGameRenderer renderer, RoundSession session ) : base (navigator ) { this .renderer = renderer; this .session = session; } public override void Enter () { session.Game.PieceLocked += OnLocked; AddMessage("Clear rows to score. SPACE: hard drop. P: pause." ); } private void AddMessage (string message ) { if (messages.Count == 5 ) messages.RemoveAt(0 ); messages.Add(message); } private void OnLocked (object ? sender, PieceLockedEventArgs e ) => AddMessage($"Placed {e.Kind} . Lines: {e.ClearedLines} . Clear bonus: {e.AwardedPoints} ." ); public override void Exit () { session.Game.PieceLocked -= OnLocked; session.Dispose(); } public override void Handle (InputCommand command ) { if (command == InputCommand.Escape) { Navigator.ShowMenu(); return ; } if (session.Game.IsGameOver) return ; if (command == InputCommand.Pause) { Paused = !Paused; accumulated = TimeSpan.Zero; AddMessage(Paused ? "Paused." : "Resumed." ); return ; } if (Paused) return ; switch (command) { case InputCommand.Left: session.Game.MoveHorizontal(-1 ); break ; case InputCommand.Right: session.Game.MoveHorizontal(1 ); break ; case InputCommand.Rotate: session.Game.Rotate(); break ; case InputCommand.SoftDrop: session.Game.SoftDrop(); break ; case InputCommand.HardDrop: session.Game.HardDrop(); accumulated = TimeSpan.Zero; break ; } } public override void Update (TimeSpan elapsed ) { if (session.Game.IsGameOver) { Navigator.EndRound(new RoundSummary(session)); return ; } if (Paused) return ; accumulated += elapsed; while (!session.Game.IsGameOver && accumulated >= TimeSpan.FromMilliseconds(session.Game.FallMilliseconds)) { accumulated -= TimeSpan.FromMilliseconds(session.Game.FallMilliseconds); session.Game.Tick(); } if (session.Game.IsGameOver) Navigator.EndRound(new RoundSummary(session)); } public override void Render () => renderer.DrawGame(session.Game, Paused, messages.AsReadOnly(), session.Statistics.Capture()); } }
TetrisGame/Scenes/Scene.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 using System;using TetrisGame.Contracts;using TetrisGame.Core;namespace TetrisGame.Scenes { public abstract class Scene { protected ISceneNavigator Navigator { get ; } protected Scene (ISceneNavigator navigator ) => Navigator = navigator; public virtual void Enter () { } public virtual void Exit () { } public abstract void Handle (InputCommand command ) ; public virtual void Update (TimeSpan elapsed ) { } public abstract void Render () ; } public sealed class StartScene : Scene { private readonly IMenuRenderer renderer; public StartScene (ISceneNavigator navigator, IMenuRenderer renderer ) : base (navigator ) => this .renderer = renderer; public override void Handle (InputCommand command ) { if (command == InputCommand.Start) Navigator.StartRound(); else if (command == InputCommand.Escape) Navigator.Quit(); } public override void Render () => renderer.DrawStart(); } public sealed class EndScene : Scene { private readonly IMenuRenderer renderer; private readonly RoundSummary summary; public EndScene (ISceneNavigator navigator, IMenuRenderer renderer, RoundSummary summary ) : base (navigator ) { this .renderer = renderer; this .summary = summary; } public override void Handle (InputCommand command ) { if (command == InputCommand.Replay) Navigator.StartRound(); else if (command == InputCommand.Start) Navigator.ShowMenu(); else if (command == InputCommand.Escape) Navigator.Quit(); } public override void Render () => renderer.DrawEnd(summary); } }
TetrisGame/Services/StatisticsWorker.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 using System;using System.Collections.Generic;using System.Threading;using TetrisGame.Core;using TetrisGame.Domain;namespace TetrisGame.Services { public sealed class StatisticsWorker : IDisposable { private readonly object gate = new object (); private readonly Queue<PieceLockedEventArgs> pending = new Queue<PieceLockedEventArgs>(); private readonly Dictionary<TetrominoKind, int > counts = new Dictionary<TetrominoKind, int >(); private readonly AutoResetEvent signal = new AutoResetEvent(false ); private readonly Thread worker; private bool stopping; private bool disposed; private long pieces, lines, clearPoints; public StatisticsWorker () { worker = new Thread(Run) { IsBackground = true , Name = "Tetris statistics" }; worker.Start(); } public void OnPieceLocked (object ? sender, PieceLockedEventArgs args ) { lock (gate) { if (stopping) return ; pending.Enqueue(args ); signal.Set(); } } private void Run () { while (true ) { bool handled = false ; lock (gate) { if (pending.Count > 0 ) { PieceLockedEventArgs item = pending.Dequeue(); counts.TryGetValue(item.Kind, out int count); counts[item.Kind] = count + 1 ; pieces++; lines += item.ClearedLines; clearPoints += item.AwardedPoints; handled = true ; } else if (stopping) return ; } if (!handled) signal.WaitOne(); } } public StatisticsSnapshot Capture () { lock (gate) { List<PieceCount> snapshot = new List<PieceCount>(); foreach (KeyValuePair<TetrominoKind, int > entry in counts) snapshot.Add(new PieceCount(entry.Key, entry.Value)); return new StatisticsSnapshot(pieces, lines, clearPoints, snapshot); } } public void Dispose () { lock (gate) { if (disposed) return ; disposed = true ; stopping = true ; signal.Set(); } worker.Join(); signal.Dispose(); } } }
TetrisGame/TetrisGame.csproj 1 2 3 <Project Sdk ="Microsoft.NET.Sdk" > <PropertyGroup > <OutputType > Exe</OutputType > <TargetFramework > net10.0</TargetFramework > <LangVersion > 12.0</LangVersion > <ImplicitUsings > disable</ImplicitUsings > <Nullable > enable</Nullable > </PropertyGroup > </Project >
C# 学习系列: C# 入门 → C# 基础 → C# 进阶 → C# 高级