Compare commits

...

6 Commits

Author SHA1 Message Date
何冠峰
322c4a9847 refactor: rename LoadRawFile API to LoadBundleFile with BundleFileHandle 2026-05-13 17:42:07 +08:00
何冠峰
6b23927f71 feat: add bundle type dropdown for editor simulate pipeline 2026-05-13 16:27:40 +08:00
何冠峰
dfa9ff6954 feat : Archive file build pipeline 2026-05-13 15:49:03 +08:00
何冠峰
4c717b69db Update CHANGELOG.md 2026-05-09 17:39:32 +08:00
何冠峰
d64997a0df Update package.json 2026-05-09 17:38:36 +08:00
何冠峰
a265b85d37 refactor : 代码重构 2026-05-09 17:30:10 +08:00
369 changed files with 6921 additions and 8182 deletions

4
.gitignore vendored
View File

@@ -15,7 +15,9 @@
/Bundles/ /Bundles/
/ProjectSettings/ /ProjectSettings/
/App/ /App/
/yoo/ /yoo/
/Assets/Docs
/Assets/Docs.meta
/Assets/StreamingAssets /Assets/StreamingAssets
/Assets/StreamingAssets.meta /Assets/StreamingAssets.meta
/Assets/Samples /Assets/Samples

File diff suppressed because it is too large Load Diff

View File

@@ -1,228 +0,0 @@
# .NET 命名准则
> 参考来源:[Microsoft .NET 框架设计准则 - 命名准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/naming-guidelines)
>
> 核心原则:**可读性优先、语义清晰、风格一致、避免歧义**
---
## 一、大小写约定
### 1.1 两种大小写风格
| 风格 | 规则 | 示例 |
|------|------|------|
| **PascalCasing** | 每个单词首字母大写(含首个单词) | `PropertyDescriptor``HtmlTag` |
| **camelCasing** | 除第一个单词外,每个单词首字母大写 | `propertyDescriptor``htmlTag` |
### 1.2 各标识符的大小写规则
| 标识符 | 大小写 | 示例 |
|--------|--------|------|
| 命名空间 | PascalCasing | `System.Security` |
| 类型(类/结构) | PascalCasing | `StreamReader` |
| 接口 | PascalCasing | `IEnumerable` |
| 方法 | PascalCasing | `ToString()` |
| 属性 | PascalCasing | `Length` |
| 事件 | PascalCasing | `Exited` |
| 字段(公共/静态) | PascalCasing | `InfiniteTimeout` |
| 枚举值 | PascalCasing | `FileMode.Append` |
| 参数 | camelCasing | `string value` |
### 1.3 首字母缩写词规则
- **两个字母**的缩写词:全部大写,如 `IO``DB`
- **三个字母及以上**的缩写词:仅首字母大写,如 `Html``Xml`
- camelCasing 中,开头的两字母缩写词全部小写:`ioStream`
### 1.4 常见复合词的正确写法
| 正确 (Pascal) | 正确 (camel) | 错误 |
|---------|---------|------|
| `Callback` | `callback` | ~~CallBack~~ |
| `Endpoint` | `endpoint` | ~~EndPoint~~ |
| `FileName` | `fileName` | ~~Filename~~ |
| `Hashtable` | `hashtable` | ~~HashTable~~ |
| `Id` | `id` | ~~ID~~ |
| `Metadata` | `metadata` | ~~MetaData~~ |
| `Namespace` | `namespace` | ~~NameSpace~~ |
| `Ok` | `ok` | ~~OK~~ |
| `UserName` | `userName` | ~~Username~~ |
| `WhiteSpace` | `whiteSpace` | ~~Whitespace~~ |
| `SignIn` | `signIn` | ~~SignOn~~ |
| `LogOff` | `logOff` | ~~LogOut~~ |
| `Email` | `email` | ~~EMail~~ |
> ✔️ 在 Unity 项目中,`ID` 保持全大写以与 Unity API 风格一致(如 `GetInstanceID()`、`PropertyToID()`),项目内应保持风格统一。
### 1.5 区分大小写
- 不要假定所有语言都区分大小写
- 公开 API 中不应仅靠大小写来区分两个名称
---
## 二、常规命名约定
### 2.1 用词选择
- **可读性优先于简洁性**`CanScrollHorizontally` 优于 `ScrollableX`
- **语义清晰**`HorizontalAlignment` 优于 `AlignmentHorizontal`
- **禁止**使用下划线 `_`、连字符 `-` 或其他非字母字符
- **禁止**使用匈牙利命名法(如 `strName``iCount`
- **避免**与编程语言关键字冲突
### 2.2 缩写和首字母缩写词
- **禁止**使用缩写:用 `GetWindow` 而非 `GetWin`
- **禁止**使用未被广泛接受的首字母缩略词
### 2.3 避免语言特定名称
- 使用语义化名称:`GetLength` 优于 `GetInt`
- 需要表示类型时,使用 CLR 类型名称而非语言关键字:`ToInt64` 而非 `ToLong`
### 2.4 现有 API 的新版本命名
- 使用类似旧 API 的名称
- **优先添加后缀**而非前缀(便于 IntelliSense 排序)
- 用**数字后缀**标识版本(如 `64` 表示 64 位版本)
- **禁止**使用 `Ex` 后缀
---
## 三、程序集和 DLL 的名称
- 选择能体现**大范围功能**的名称
- 推荐格式:`<Company>.<Component>.dll`,如 `Litware.Controls.dll`
- 基于程序集所含命名空间的**公共前缀**来命名
---
## 四、命名空间的名称
- 推荐格式:`<Company>.(<Product>|<Technology>)[.<Feature>][.<Subnamespace>]`
- 使用 **PascalCasing**,用句点分隔:`Microsoft.Office.PowerPoint`
- 使用**与版本无关的稳定产品名称**
- 适当使用**复数**`System.Collections` 而非 `System.Collection`
- **禁止**命名空间与其内部类型同名
- **禁止**使用泛型名称如 `Element``Node``Log``Message`,应加限定:`FormElement``XmlNode``EventLog`
---
## 五、类、结构和接口的名称
### 5.1 类和结构
- 使用 PascalCasing**名词或名词短语**
- **禁止**添加 `C` 前缀
- 考虑以**基类名称结尾**`ArgumentOutOfRangeException`(派生自 `Exception`
### 5.2 接口
- 使用**形容词短语**,偶尔使用名词
- **必须**以 `I` 前缀开头:`IComponent``ICustomAttributeProvider``IPersistable`
### 5.3 泛型类型参数
- 单字母参数时用 `T``IComparer<T>`
- 描述性名称以 `T` 开头:`ISessionChannel<TSession>`
- 考虑在名称中体现约束:`TSession`(约束为 `ISession`
### 5.4 常见类型的后缀规则
| 基类/接口 | 后缀要求 |
|-----------|---------|
| `System.Attribute` | 添加 `Attribute` 后缀 |
| `System.Delegate`(事件用) | 添加 `EventHandler` 后缀 |
| `System.Delegate`(回调用) | 添加 `Callback` 后缀 |
| `System.EventArgs` | 添加 `EventArgs` 后缀 |
| `System.Exception` | 添加 `Exception` 后缀 |
| `IDictionary` / `IDictionary<TKey,TValue>` | 添加 `Dictionary` 后缀 |
| `IEnumerable` / `ICollection` / `IList` | 添加 `Collection` 后缀 |
| `System.IO.Stream` | 添加 `Stream` 后缀 |
### 5.5 枚举命名
- 普通枚举用**单数**名称:`FileMode`
- 位标志枚举用**复数**名称:`FileAttributes`
- **禁止**添加 `Enum``Flag``Flags` 后缀
- **禁止**在枚举值上使用前缀
- ✔️ 允许对枚举类型名称添加 `E` 前缀以提高可辨识度,项目内应保持风格一致
---
## 六、类型成员的名称
### 6.1 方法
- 使用**谓词或谓词短语**`CompareTo``Split``Trim`
### 6.2 属性
- 使用**名词、名词短语或形容词**
- 布尔属性使用肯定短语:`CanSeek` 而非 `CantSeek`
- 布尔属性可加 `Is``Can``Has` 前缀(仅在有意义时)
- 可以与类型同名:`public Color Color { get; set; }`
- 集合属性用**复数**`Items` 而非 `ItemList`
### 6.3 事件
- 使用**谓词或谓词短语**
- 用时态区分前后:`Closing`(关闭前)、`Closed`(关闭后)
- **禁止**用 `Before` / `After` 前后缀
- 事件处理程序委托添加 `EventHandler` 后缀
- 事件参数类添加 `EventArgs` 后缀
- 事件处理程序参数命名为 `sender``e`
### 6.4 字段
- 使用 PascalCasing
- 使用**名词、名词短语或形容词**
- **禁止**使用前缀(如 `g_``s_``m_`
---
## 七、参数命名
- 使用 **camelCasing**
- 使用**描述性名称**
- 根据**含义**而非类型命名
- 二元运算符重载:无特殊含义时用 `left` / `right`
- 一元运算符重载:无特殊含义时用 `value`
- **禁止**对运算符参数使用缩写或数字索引
---
## 八、资源命名
- 使用 **PascalCasing**
- 使用**描述性标识符**
- 仅使用字母数字字符和下划线
- 异常消息资源格式:`异常类型名 + 短标识符`,如 `ArgumentExceptionIllegalCharacters`
---
## 九、内部字段命名约定
以上命名准则主要适用于公开 API。对于内部实现代码以下前缀约定对代码审查者非常有价值
| 字段类型 | 前缀 | 示例 |
|---------|------|------|
| 私有/内部**实例**字段 | `_` | `_count``_name` |
| 私有/内部**静态**字段 | `s_` | `s_instance``s_defaultValue` |
| 私有/内部**线程静态**字段 | `t_` | `t_cachedBuffer` |
```csharp
public class ConnectionPool
{
private static readonly int s_maxPoolSize = 100;
[ThreadStatic]
private static Queue<Connection> t_localPool;
private readonly string _connectionString;
private int _activeCount;
}
```
> 注意:此约定仅适用于**私有和内部**字段,不适用于公共 API。公共字段如 `public static readonly` 字段)仍然使用 PascalCasing 且不带前缀,这与前文"六、类型成员的名称 - 6.4 字段"中"禁止使用前缀"的准则不矛盾。

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: b6b6bc56d8bfe6c4fa3776f98ab3f822
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,279 +0,0 @@
# .NET 类型设计准则
> 参考来源:[Microsoft .NET 框架设计准则 - 类型设计准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/type)
>
> 核心原则:**每种类型都应是一组定义完善的相关成员,而非不相关功能的随机集合。**
---
## 一、类型概述
从 CLR 的角度看只有两类类型(引用类型和值类型),但在框架设计中可细分为以下逻辑组:
| 类型 | 说明 |
|------|------|
| **类Class** | 引用类型的一般情况,支持丰富的面向对象功能,构成框架中的大部分类型 |
| **接口Interface** | 可由引用类型和值类型实现,用作多态层次结构的根,模拟多重继承 |
| **结构Struct** | 值类型的一般情况,应保留为小型简单类型,类似语言基元 |
| **枚举Enum** | 特殊值类型,用于定义一组简短的命名常量 |
| **静态类Static Class** | 仅包含静态成员的容器,用于提供操作快捷方式 |
| **委托/异常/属性/数组/集合** | 用于特定用途的引用类型特殊情况 |
---
## 二、在类和结构之间进行选择
### 2.1 引用类型 vs 值类型的核心区别
| 特征 | 引用类型(类) | 值类型(结构) |
|------|--------------|--------------|
| **分配位置** | 堆上分配,由 GC 回收 | 栈上分配或内联在包含类型中 |
| **数组分配** | 元素是引用,实例在堆上 | 元素是实际实例,内联分配 |
| **赋值行为** | 复制引用 | 复制整个值 |
| **传递方式** | 按引用传递 | 按值传递(产生副本) |
| **装箱** | 无装箱开销 | 转换为接口或引用类型时会装箱 |
### 2.2 选择准则
✔️ 如果类型的实例**较小**且通常**生存期较短**或通常**嵌入在其他对象中**,请考虑定义结构而不是类。
❌ **避免**定义结构,除非类型**同时满足**以下所有特征:
- 逻辑上表示单个值,类似基元类型(`int``double`
- 实例大小**低于 24 字节**
- 是**不可变**的
- 不必**频繁装箱**
> 在所有其他情况下,应将类型定义为类。
---
## 三、抽象类设计
❌ **禁止**在抽象类型中定义 `public``protected internal` 构造函数。
> 抽象类型无法被实例化,公共构造函数会对用户产生误导。
✔️ **必须**在抽象类中定义 `protected``internal` 构造函数。
- `protected` 构造函数更常用,允许基类在创建子类型时执行初始化
- `internal` 构造函数可将具体实现限制在定义类的程序集内
✔️ **必须**提供至少一种从你交付的每个抽象类继承的具体类型。
> 例如 `FileStream` 是抽象类 `Stream` 的具体实现,这有助于验证抽象类的设计。
---
## 四、静态类设计
静态类仅包含静态成员(除继承自 `Object` 的实例成员和私有构造函数外)。在 C# 中,声明为 `static` 的类是密封的、抽象的,且无法重写或声明实例成员。
### 适用场景
- 提供其他操作的快捷方式(如 `System.IO.File`
- 扩展方法的持有者
- 无法确保完整面向对象包装器的功能(如 `System.Environment`
### 设计准则
✔️ **谨慎**使用静态类,仅作为框架面向对象核心的支持类。
❌ **禁止**将静态类视为杂项存储桶。
❌ **禁止**在静态类中声明或重写实例成员。
✔️ 如果语言没有对静态类的内置支持,则将其声明为 `sealed abstract` 并添加私有实例构造函数。
---
## 五、接口设计
### 5.1 适用场景
- 需要一组**包含值类型**的类型支持通用 API
- 需要在已从其他类型继承的类型上支持功能(模拟多重继承)
### 5.2 设计准则
✔️ 如果需要包含值类型的多种类型支持通用 API请定义接口。
✔️ 如果需要在已有基类继承的类型上支持功能,请考虑定义接口。
✔️ **必须**至少提供一种类型作为接口的实现。
> 例如 `List<T>` 是 `IList<T>` 的实现。
✔️ **必须**至少提供一个使用该接口的 API以接口为参数的方法或类型为接口的属性
> 例如 `List<T>.Sort` 使用 `IComparer<T>` 接口。
❌ **避免**使用标记接口没有成员的接口应改用自定义属性Attribute
❌ **禁止**向已发布的接口添加新成员。
> 这样做会破坏现有实现,应创建新接口以避免版本控制问题。
✔️ 优先定义**类**而非接口。基于类的 API 可以更容易地演进——向类添加成员不会破坏现有代码,而向接口添加成员则会。
✔️ 使用**抽象类**而非接口来将协定与实现解耦。
❌ **禁止**在公共 API 中使用 `ICloneable`
> `ICloneable` 的问题在于:有些实现是浅拷贝,有些是深拷贝,调用者永远不知道会得到什么。这使得该接口实际上毫无用处。如果需要克隆机制,请定义类型特定的 `Clone` 方法。
> **一般规则**:在设计可重用库时,通常应选择类而非接口。
---
## 六、结构设计
❌ **禁止**为结构提供无参数构造函数。
> 遵循此准则可以创建结构数组,而无需在每个元素上运行构造函数。
❌ **禁止**定义可变值类型。
> 可变值类型问题:属性 getter 返回副本,开发者可能不知道正在修改副本而非原始值。
✔️ **必须**确保所有实例数据设置为零/false/null 的状态有效。
> 防止创建结构数组时意外产生无效实例。
✔️ 如果值类型会参与相等比较(如用作字典键、放入集合、使用 `==` 运算符),**必须**实现 `IEquatable<T>`,并同时重写 `Equals(object)``GetHashCode()`
> `Object.Equals` 在值类型上会导致装箱且效率低下(使用反射),`IEquatable<T>.Equals` 性能更好且无装箱。
❌ 对于仅作为数据载体、不参与相等比较的值类型,**不必**实现 `IEquatable<T>`
❌ **禁止**显式扩展 `ValueType`
✔️ 声明不可变值类型时使用 **`readonly struct`** 修饰符。
> 编译器理解 `readonly` 修饰符,在对 `readonly` 字段调用方法等操作时可避免产生额外的防御性复制。
```csharp
public readonly struct ZipCode
{
public ZipCode(string value) => Value = value;
public string Value { get; }
public override string ToString() => Value;
}
```
✔️ 对可变值类型的非可变方法标记 **`readonly`** 修饰符。
> 允许编译器在调用时跳过值复制。
```csharp
public struct Point
{
public float X;
public float Y;
public readonly override string ToString() => $"({X}, {Y})";
}
```
❌ **避免**定义 `ref struct`ref-like 值类型),除非用于低级性能关键场景。
> `ref struct` 只能存在于栈上,不能被装箱到堆中。因此不能用作其他类型的字段(除了其他 `ref struct`),也不能在 `async` 方法中使用。
> **总结**:结构适用于小型、单个、不可变且不频繁装箱的值。
---
## 七、枚举设计
### 7.1 两种枚举类型
| 类型 | 说明 | 命名 | 示例 |
|------|------|------|------|
| **简单枚举** | 表示小型封闭式选项集 | 单数名词 | `FileMode` |
| **标志枚举** | 支持对枚举值执行位运算 | 复数名词 | `FileAttributes` |
### 7.2 通用准则
✔️ 使用枚举对表示值集的参数、属性和返回值进行**强类型化**。
✔️ **优先**使用枚举而不是静态常量。
✔️ 为简单枚举提供一个**零值**(通常命名为 `None`)。
✔️ 考虑使用 `Int32` 作为枚举的基础类型(除非有特殊需求)。
❌ **禁止**对开放集合使用枚举(如操作系统版本、好友姓名等)。
❌ **禁止**提供用于将来使用的保留枚举值。
❌ **避免**公开只有一个值的枚举。
❌ **禁止**在枚举中包含哨兵值。
❌ **禁止**直接扩展 `System.Enum`
### 7.3 标志枚举设计
✔️ **必须**将 `[Flags]` 特性应用于标志枚举,不要应用于简单枚举。
✔️ **必须**对标志枚举值使用 **2 的幂**,以便用按位 OR 自由组合。
✔️ 考虑为常用标志组合提供特殊枚举值(如 `ReadWrite = Read | Write`)。
✔️ **必须**将标志枚举的零值命名为 `None`,表示"所有标志均已清除"。
❌ **避免**在某些值组合无效的情况下创建标志枚举。
❌ **避免**使用标志枚举值零,除非它表示"所有标志已清除"且命名为 `None`
### 7.4 向枚举添加值
✔️ 考虑向已发布的枚举添加值(兼容性风险通常较小)。
> 如果有不兼容风险,可添加新 API 返回新旧值,并弃用旧 API。
---
## 八、嵌套类型
嵌套类型在另一个类型的作用域内定义,可访问封闭类型的所有成员(包括私有字段)。
### 8.1 适用场景
- 对封闭类型的**实现细节**进行建模
- 例如:集合的枚举器可以是该集合的嵌套类型
### 8.2 设计准则
✔️ 当嵌套类型与外部类型之间的关系使得**成员可访问性语义**可取时,使用嵌套类型。
❌ **禁止**将公共嵌套类型用作逻辑分组构造(应使用命名空间)。
❌ **避免**公开的嵌套类型(极少数子类化或高级自定义方案除外)。
❌ 如果某个类型可能在其包含类型**之外被引用**,不要使用嵌套类型。
> 例如,传递给类方法的枚举不应定义为该类的嵌套类型。
❌ **禁止**使用需要通过客户端代码实例化的嵌套类型。
> 如果类型有公共构造函数,通常不应被嵌套。
❌ **禁止**将嵌套类型定义为接口的成员。
> 多种语言不支持此类构造。
---
## 九、速查表
| 场景 | 推荐类型 |
|------|---------|
| 大部分通用情况 | 类Class |
| 小型、不可变、类似基元的值 | 结构Struct |
| 需要值类型参与多态 | 接口Interface |
| 需要模拟多重继承 | 接口Interface |
| 封闭的命名常量集 | 枚举Enum |
| 可组合的位标志 | 标志枚举([Flags] Enum |
| 纯静态工具方法容器 | 静态类Static Class |
| 提供可扩展的基础实现 | 抽象类Abstract Class |
| 封闭类型的内部实现细节 | 嵌套类型Nested Type |

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 982ed5c869a8a6c4d8adda239881d163
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,339 +0,0 @@
# .NET 成员设计准则
> 参考来源:[Microsoft .NET 框架设计准则 - 成员设计准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/member)
>
> 方法、属性、事件、构造函数和字段统称为成员,是将框架功能展现给最终用户的手段。
---
## 一、成员重载
成员重载是指在同一类型上创建两个或多个仅参数数量或类型不同、但名称相同的成员。只有**方法、构造函数和索引属性**可以重载。
### 设计准则
✔️ 使用**描述性参数名称**来指示较短重载使用的默认值。
✔️ 仅使**最长的重载虚拟化**(如果需要扩展性),较短的重载应直接调用较长的重载。
✔️ 允许 `null` 作为可选参数传递。
✔️ 优先使用**成员重载**,而不是带默认参数的成员(默认参数不符合 CLS
❌ **避免**在重载中任意改变参数名称。相同输入的参数在所有重载中应保持相同名称。
❌ **避免**重载中参数顺序不一致。相同名称的参数应在所有重载中处于相同位置。
❌ **禁止**使用 `ref``out` 修饰符来区分重载。
❌ **禁止**使用位置相同、类型相似但语义不同的重载。
---
## 二、属性设计
属性在技术上与方法相似,但使用场景不同。它们应被视为"智能字段",兼具字段的调用语法和方法的灵活性。
### 2.1 通用准则
✔️ 如果调用方不应更改属性的值,请创建**只读属性**get-only
✔️ 为所有属性提供**合理的默认值**,确保不会导致安全漏洞或低效代码。
✔️ 允许按**任意顺序**设置属性,即使这会导致对象临时处于无效状态。
✔️ 如果属性 setter 引发异常,请**保留以前的值**。
❌ **禁止**提供仅有 setter 的属性,或 setter 比 getter 可访问性更广的属性。
> 如果无法提供 getter请改用以 `Set` 开头的方法,如 `SetCachePath`。
❌ **避免**在属性 getter 中引发异常。
> getter 应是简单操作,无前置条件。如果可能引发异常,应重新设计为方法。
### 2.2 索引器设计
✔️ 考虑使用索引器提供对内部数组中数据的访问。
✔️ 考虑为表示项集合的类型提供索引器。
✔️ 索引器默认使用 `Item` 作为名称(除非有明显更好的名称)。
❌ **避免**使用多个参数的索引属性(如需多参数,请改用 `Get`/`Set` 方法)。
❌ **避免**使用 `Int32``Int64``String``Object` 或枚举以外类型作为索引器参数。
❌ **禁止**同时提供索引器和语义等效的方法。
❌ **禁止**在同一类型中提供多个重载索引器系列。
### 2.3 属性更改通知事件
✔️ 考虑在修改高级 API通常是设计器组件中的属性值时引发**更改通知事件**。
✔️ 考虑在属性值因外部因素更改时引发更改通知事件。
---
## 三、构造函数设计
### 3.1 实例构造函数
✔️ 考虑提供**简单的**(理想情况下为默认)构造函数,参数少且都是基元或枚举类型。
✔️ 如果操作语义不直接映射到新实例的构造,考虑使用**静态工厂方法**代替构造函数。
✔️ 使用构造函数参数作为设置主属性的**快捷方式**。
✔️ 如果构造函数参数仅用于设置属性,则对参数和属性使用**相同的名称**(仅大小写不同)。
✔️ 在构造函数中执行**最少的工作**,其他处理应延迟到需要时。
✔️ 在适当时从实例构造函数**抛出异常**。
✔️ 如果需要无参数构造函数,请**显式声明**(避免添加参数化构造函数后隐式丢失)。
❌ **避免**在结构上显式定义无参数构造函数(使数组创建更快)。
**避免**在构造函数内对对象调用**虚拟成员**
> 调用虚拟成员会导致调用派生度最高的重写,即使该派生类的构造函数尚未完全执行。
### 3.2 类型构造函数(静态构造函数)
✔️ 将静态构造函数设为**私有**。
✔️ 考虑**内联初始化**静态字段,而非使用显式静态构造函数(运行时可优化性能)。
❌ **禁止**从静态构造函数引发异常。
> 如果类型构造函数抛出异常,该类型在当前应用程序域中将不可用。
---
## 四、事件设计
事件是最常见的回调形式,允许框架调用用户代码。
### 4.1 两种事件时机
| 类型 | 说明 | 示例 |
|------|------|------|
| **预事件** | 状态变更**之前**引发 | `Form.Closing` |
| **后事件** | 状态变更**之后**引发 | `Form.Closed` |
### 4.2 设计准则
✔️ 对事件使用"**raise**"(引发)一词,而非 "fire" 或 "trigger"。
✔️ 使用 `EventHandler<TEventArgs>` 而非手动创建新委托。
✔️ 在 Unity 项目中,可以使用 `Action<T>` 代替 `EventHandler<TEventArgs>` 作为事件委托类型,以保持与 Unity 生态惯例一致。
✔️ 考虑使用 `EventArgs` 的子类作为事件参数(即使最初为空,也便于将来扩展)。
✔️ 使用**受保护的虚拟方法**引发每个事件(仅适用于未密封类的非静态事件)。
> 方法命名约定:以 `On` 开头 + 事件名称,如 `OnClosing`。
✔️ 该方法接受一个参数,命名为 `e`,类型为事件参数类。
✔️ 考虑引发最终用户可**取消**的预事件(使用 `CancelEventArgs` 或其子类)。
❌ 引发非静态事件时,**禁止**将 `null` 作为 sender 传递。
✔️ 引发静态事件时,将 `null` 作为 sender 传递。
❌ 引发事件时,**禁止**将 `null` 作为事件数据参数传递(应使用 `EventArgs.Empty`)。
### 4.3 自定义事件处理程序
✔️ 事件处理程序返回类型为 `void`
✔️ 第一个参数类型为 `object`,命名为 `sender`
✔️ 第二个参数类型为 `EventArgs` 或其子类,命名为 `e`
❌ 事件处理程序**不超过两个参数**。
---
## 五、字段设计
封装原则要求存储在对象中的数据只能通过该对象访问。
### 设计准则
❌ **禁止**提供 `public``protected` 的实例字段(应使用属性代替)。
✔️ 在 Unity 项目中,需要通过 `JsonUtility` 序列化的 `[Serializable]` 类型,应使用公共字段而非属性(`JsonUtility` 不支持属性序列化)。
✔️ 对永远不会更改的常量使用 `const` 字段。
> 编译器将 const 值直接嵌入调用代码,更改 const 值会破坏兼容性。
✔️ 对预定义的对象实例使用 `public static readonly` 字段。
❌ **禁止**将可变类型的实例赋给 `readonly` 字段。
> `readonly` 仅阻止引用被替换,不阻止通过成员修改实例数据。例如数组、集合等可变类型的实例数据仍可被修改。
---
## 六、扩展方法
扩展方法允许使用实例方法调用语法调用静态方法,定义在静态"发起方"类中。
### 适用场景
✔️ 为接口的每个实现提供辅助功能(基于核心接口编写),如 LINQ 运算符。
✔️ 当实例方法会引入对某类型的依赖,且该依赖会破坏依赖管理规则时。
### 设计准则
❌ **避免**轻率地定义扩展方法,尤其是在你不拥有的类型上。
> 如果拥有类型源代码,考虑使用常规实例方法。
❌ **避免**在 `System.Object` 上定义扩展方法VB 用户无法使用扩展方法语法调用)。
❌ **禁止**将扩展方法放在与扩展类型相同的命名空间中(除非用于接口或依赖项管理)。
❌ **避免**使用相同签名定义多个扩展方法,即使在不同命名空间中。
❌ **避免**使用泛型命名空间名(如 "Extensions"),应使用描述性名称(如 "Routing")。
✔️ 如果类型是接口且扩展方法用于大多数场景,考虑放在与扩展类型相同的命名空间中。
---
## 七、运算符重载
运算符重载允许框架类型表现得像内置语言基元。
### 7.1 通用准则
✔️ 考虑在感觉像**基元类型**的类型中定义运算符重载。
✔️ 在表示**数字**的结构中定义运算符重载(如 `Decimal`)。
✔️ **对称**重载运算符:重载 `==` 则必须重载 `!=`,重载 `<` 则必须重载 `>`
✔️ 考虑为每个重载运算符提供具有**友好名称**的等效方法(因为某些语言不支持运算符重载)。
❌ **避免**定义运算符重载,除非类型感觉像基元类型。
❌ 定义运算符重载时**不要花哨**,操作结果应显而易见。
❌ 除非至少一个操作数属于定义重载的类型,否则**禁止**提供运算符重载。
### 7.2 常用运算符与友好方法名对照
| 运算符 | 友好方法名 |
|--------|----------|
| `+` (二元) | `Add` |
| `-` (二元) | `Subtract` |
| `*` (二元) | `Multiply` |
| `/` | `Divide` |
| `%` | `Mod` / `Remainder` |
| `==` | `Equals` |
| `!=` | `Equals` |
| `<` / `>` | `CompareTo` |
| `<=` / `>=` | `CompareTo` |
| `++` | `Increment` |
| `--` | `Decrement` |
| `- (一元)` | `Negate` |
### 7.3 转换运算符
❌ 如果最终用户**未明确预期**该转换,禁止提供转换运算符。
❌ **禁止**在类型域之外定义转换运算符(如不应将 `Double` 转换为 `DateTime`)。
❌ 如果转换**可能丢失数据**,禁止提供隐式转换运算符(可提供显式转换)。
❌ **禁止**从隐式强制转换引发异常。
✔️ 如果显式强制转换导致有损转换,且运算符契约不允许有损,请抛出 `InvalidCastException`
---
## 八、参数设计
### 8.1 通用准则
✔️ 使用提供成员所需功能的**最低派生参数类型**。
> 例如用 `IEnumerable` 而非 `ArrayList` 或 `IList` 作为枚举方法的参数。
✔️ 将所有 `out` 参数置于所有按值和 `ref` 参数**之后**。
✔️ 在重写成员或实现接口时,参数命名保持**一致**。
❌ **禁止**使用保留参数(将来需要更多输入时,添加新重载即可)。
❌ **禁止**公开采用指针、指针数组或多维数组作为参数的方法。
### 8.2 枚举 vs 布尔参数
✔️ 如果不使用枚举则成员会有两个或更多布尔参数,请改用**枚举**。
✔️ 考虑为构造函数参数使用布尔值(真正的双状态值,仅用于初始化布尔属性时)。
❌ **禁止**使用布尔值,除非绝对确定永远不需要两个以上的值。
```csharp
// 错误:布尔参数含义不明确
Stream stream = File.Open("foo.txt", true, false);
// 正确:使用枚举,语义清晰
Stream stream = File.Open("foo.txt", CasingOptions.CaseSensitive, FileMode.Open);
```
### 8.3 参数验证
✔️ 验证传递到 `public``protected` 或显式实现成员的参数,验证失败则抛出 `ArgumentException` 或其子类。
✔️ 如果传入 `null` 且成员不支持,抛出 `ArgumentNullException`
✔️ 验证枚举参数CLR 允许将任何整数值转换为枚举值)。
✔️ 注意可变参数在验证后可能已更改(安全敏感成员应先创建副本)。
❌ **禁止**使用 `Enum.IsDefined` 进行枚举范围检查。
### 8.4 参数传递方式
| 方式 | 说明 |
|------|------|
| **按值** (默认) | 成员收到参数副本 |
| **ref** | 成员收到参数引用,可修改调用方传递的参数 |
| **out** | 类似 ref但初始未赋值必须在返回前赋值 |
❌ **避免**使用 `out``ref` 参数(需要指针经验,且两者区别未被广泛理解)。
❌ **禁止**按引用传递引用类型(有限例外:如交换引用的方法)。
❌ **禁止**对值类型使用只读引用传递(`in`)。
> 结构本身应设计为小型,按值传递的开销很低。使用 `in` 反而增加了 API 的复杂性,收益甚微。
### 8.5 可变参数数params
✔️ 如果希望最终用户传递少量元素的数组,考虑添加 `params` 关键字。
✔️ 考虑在简单重载中使用 `params`,即使更复杂的重载不使用。
✔️ 尝试对参数排序以便使用 `params`
✔️ 考虑在性能敏感的 API 中为少量参数提供特殊重载(避免创建临时数组)。
✔️ 注意 `null` 可作为 params 数组传递,处理前应验证非 null。
❌ 如果调用方几乎总是已有数组作为输入,不要使用 `params`
❌ 如果成员会修改 params 数组,不要使用 `params`(临时数组的修改会丢失)。
❌ **禁止**使用 `varargs`(省略号)方法(不符合 CLS 标准)。

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: f6b12280cb6fd3c42bfc80626a6eaf54
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,268 +0,0 @@
# .NET 扩展设计准则
> 参考来源:[Microsoft .NET 框架设计准则 - 针对扩展性进行设计](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/designing-for-extensibility)
>
> 核心原则:**选择满足需求的最低成本扩展机制。可以在将来增加扩展性,但永远无法在不引入重大变更的情况下去除它。**
---
## 一、扩展性机制概述
框架中可通过多种方式实现扩展性,从低成本到高成本依次为:
| 机制 | 成本 | 能力 | 说明 |
|------|------|------|------|
| **非密封类** | 低 | 低 | 允许继承添加便捷成员 |
| **受保护成员** | 低 | 低-中 | 为子类提供高级自定义选项 |
| **事件** | 中 | 中 | 便捷一致的回调语法,与 IDE 集成 |
| **回调(委托)** | 中 | 中 | 允许用户提供自定义执行代码 |
| **虚拟成员** | 中-高 | 中-高 | 允许子类替代行为,适合特化场景 |
| **抽象(抽象类/接口)** | 高 | 高 | 最强大的扩展性是插件、IoC、管道等模式的核心 |
---
## 二、非密封类
默认情况下,类在大多数编程语言中是非密封的,这也是框架中大多数类的推荐默认值。
### 设计准则
✔️ 考虑使用没有额外虚拟成员或受保护成员的**非密封类**,作为一种为框架提供既经济又受欢迎的扩展性方法。
> 开发人员通常希望继承自非密封类,以便添加自定义构造函数、新方法或方法重载。例如 `System.Messaging.MessageQueue` 未密封,允许用户创建默认为特定队列路径的自定义队列。
> 非密封类型的测试成本相对较低,框架用户非常赞赏其提供的扩展性。
---
## 三、受保护成员
受保护成员本身不提供扩展性,但可以通过子类化来增强扩展性,用于公开高级自定义选项而无需复杂化主公共接口。
### 设计准则
✔️ 考虑使用受保护成员进行**高级自定义**。
✔️ 出于安全、文档和兼容性分析的目的,应将未密封类中的受保护成员**视为公共成员**。
> 任何人都可以继承未密封类并访问其受保护成员,因此公共成员所用的所有防御性编码做法同样适用于受保护成员。
---
## 四、事件和回调
回调是允许框架通过委托回调到用户代码的扩展点,通常通过方法参数传递委托。事件是回调的特例,提供便捷一致的语法。
### 设计准则
✔️ 考虑使用**回调**来允许用户提供框架要执行的自定义代码。
✔️ 考虑使用**事件**来允许用户自定义框架行为,无需了解面向对象设计。
✔️ **优先使用事件**而非普通回调,因为事件对更广泛的开发人员更熟悉,且与 Visual Studio 语句完成集成。
✔️ 定义带回调的 API 时,使用 `Func<...>``Action<...>``Expression<...>` 类型,而非自定义委托。
✔️ 衡量并了解使用 `Expression<...>` 对性能的影响。
> `Expression<...>` 在逻辑上等效于 `Func<...>` 和 `Action<...>` 委托,主要区别在于表达式适用于远程进程或计算机中求值的场景。
✔️ 理解调用委托即在执行**任意代码**,这可能导致安全性、正确性和兼容性问题。
❌ **避免**在性能敏感的 API 中使用回调。
---
## 五、虚拟成员
虚拟成员可以被替代以更改子类行为,在扩展性方面类似回调,但执行性能和内存消耗更优。
### 优缺点
| 方面 | 说明 |
|------|------|
| **优势** | 性能优于回调和事件;在特化场景中感觉更自然 |
| **劣势** | 行为只能在编译时修改(回调可在运行时修改);设计、测试和维护成本高;任何调用都可能被不可预测地替代 |
### 设计准则
❌ **禁止**无充分理由地使成员成为虚拟成员,需充分了解设计、测试和维护虚拟成员的所有成本。
> 虚拟成员对可进行的更改不太宽容,且比非虚拟成员慢(无法内联)。
✔️ 将扩展性限制在**绝对必要的范围内**。
✔️ 对虚拟成员优先选择 **`protected`** 访问级别而非 `public`
> 公共成员应通过调用受保护的虚拟成员来提供扩展性。这样将虚拟扩展点的范围限定在可使用的位置。
```csharp
// 推荐模式:公共方法调用受保护的虚拟方法
public class Control
{
public void Draw()
{
// 公共 API 逻辑...
OnDraw();
}
protected virtual void OnDraw()
{
// 子类可替代此方法来自定义绘制行为
}
}
```
---
## 六、抽象(抽象类型和接口)
抽象是描述协定但不提供完整实现的类型通常作为抽象类或接口实现。它们提供最强大的扩展性是插件、IoC、管道等架构模式的核心。
### 设计注意事项
- 有意义且经得起时间考验的抽象**很难设计**
- 成员过多 → 难以实现;成员过少 → 许多场景中无用
- 框架中抽象过多会**负面影响可用性**
- 良好的抽象对框架的**可测试性**非常重要(便于单元测试中替代依赖)
### 设计准则
**禁止**提供抽象,除非已通过开发**多个具体实现和 API** 对其进行测试验证。
✔️ 设计抽象时,在**抽象类和接口**之间仔细选择。
✔️ 考虑为抽象的具体实现提供**参考测试**,允许用户验证其实现是否正确实现了协定。
---
## 七、实现抽象的基类
基类主要设计为提供通用抽象或其他类的默认实现,位于继承层次结构的中间——根部的抽象和底部的自定义实现之间。
### 典型场景
```
IList<T> ← 抽象(接口)
Collection<T> ← 基类(提供默认实现)
MyCustomCollection ← 具体实现
```
> 例如 `Collection<T>` 和 `KeyedCollection<TKey,TItem>` 是 `IList<T>` 的实现辅助基类,降低了实现自定义集合的难度。
### 设计准则
✔️ 考虑使基类为 **`abstract`**,即使不包含抽象成员。
> 清楚地向用户传达该类专为继承设计。
✔️ 考虑将基类放置在**独立于主线场景类型的命名空间**中。
> 基类适用于高级扩展性场景,对大多数用户不感兴趣。
❌ 如果类用于公共 API**避免**使用 `Base` 后缀命名基类。
> 仅当基类为框架用户提供重要价值时才应使用。如果只对框架实现者有价值,应考虑**委托内部实现**而非继承。
---
## 八、密封
密封是限制扩展性的强大机制,可以密封整个类或单个成员。
### 8.1 密封类
❌ **禁止**无充分理由地密封类。
> "想不出扩展性场景"不是充分理由。框架用户喜欢出于各种原因继承类(如添加便捷成员)。
**密封类的充分理由**
| 理由 | 说明 |
|------|------|
| 静态类 | 静态类应声明为密封 |
| 安全敏感的受保护成员 | 类在继承的受保护成员中存储安全敏感信息 |
| 大量虚拟成员 | 逐个密封的成本超过保持非密封的好处 |
| 需要快速运行时查找的属性类 | 密封属性的性能略高于非密封的 |
### 8.2 密封成员
✔️ 考虑**密封你替代override的成员**。
> 引入虚拟成员可能导致的问题同样适用于替代(尽管程度稍低),密封替代可从继承层次结构的该点开始避免这些问题。
### 8.3 密封类型的约束
❌ **禁止**在密封类型上声明 `protected` 成员或 `virtual` 成员。
> 密封类型不可被继承,受保护成员无法被调用,虚拟方法无法被替代。
---
## 九、模板方法模式
模板方法模式允许子类在保留算法整体结构的同时重新定义其中的特定步骤。其核心目标是**控制扩展性**——将扩展点集中到单个受保护的虚拟方法中。
### 设计准则
❌ **避免**将 `public` 成员设为 `virtual`
✔️ 考虑使用**模板方法模式**提供更可控的扩展性。
✔️ 为非虚公共成员提供扩展点的受保护虚拟方法,命名时使用 **`Core` 后缀**或 **`Internal` 前缀**。
✔️ 在非虚方法中完成**参数验证和状态检查**,然后再调用虚拟方法。
```csharp
// 示例1Core 后缀风格
public class Control
{
// 公共非虚方法:执行验证,调用虚拟扩展点
public void SetBounds(int x, int y, int width, int height)
{
if (width < 0) throw new ArgumentOutOfRangeException(nameof(width));
if (height < 0) throw new ArgumentOutOfRangeException(nameof(height));
SetBoundsCore(x, y, width, height);
}
// 受保护虚拟方法:子类可替代的扩展点
protected virtual void SetBoundsCore(int x, int y, int width, int height)
{
// 默认实现...
}
}
// 示例2Internal 前缀风格
public abstract class AsyncOperationBase
{
internal void StartOperation()
{
// 状态检查、异常隔离...
InternalStart();
}
protected abstract void InternalStart();
}
```
> 非虚公共方法可确保特定代码在虚拟成员调用前后执行,并确保虚拟成员按固定顺序执行。常见错误是将多个重载都设为虚拟方法——应将扩展性集中到单个方法。
---
## 十、扩展机制选择速查表
| 需求 | 推荐机制 |
|------|---------|
| 允许用户添加便捷方法 | 非密封类 |
| 提供高级自定义选项 | 受保护成员 |
| 允许用户自定义行为(不需要 OOP 知识) | 事件 |
| 允许用户提供自定义执行逻辑 | 回调Func/Action |
| 允许子类特化行为(编译时) | 虚拟成员 |
| 定义可由多种类型实现的协定 | 抽象类 / 接口 |
| 降低实现抽象的难度 | 基类 |
| 限制扩展性 | 密封sealed |
> **原则**:优先选择低成本机制。在不确定时保持非密封,因为增加扩展性容易,但移除扩展性会引入破坏性变更。

View File

@@ -1,255 +0,0 @@
# .NET 异常设计准则
> 参考来源:[Microsoft .NET 框架设计准则 - 异常设计准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/exceptions)
>
> 核心原则:**异常处理比基于返回值的错误报告具有更多优势。框架应使用异常报告所有错误条件,包括执行错误。**
---
## 一、异常引发
### 1.1 基本原则
✔️ **必须**通过引发异常来报告执行失败。
> 当成员无法完成其设计用途(成员名称所暗示的内容)时,就是执行失败。例如 `OpenFile` 无法返回打开的文件句柄。
✔️ 如果代码遇到进一步执行不安全的情况,考虑调用 `System.Environment.FailFast` 终止进程,而不是引发异常。
✔️ 考虑引发异常对性能的影响。
> **每秒抛出速率超过 100 次**可能会显著影响大多数应用程序的性能。
✔️ **必须**记录所有由公开成员因违反成员协定而引发的异常,并将其视为协定的一部分。
> 协定中的异常不应在版本间更改(异常类型不变、不添加新异常)。
❌ **禁止**返回错误代码。异常是框架中报告错误的主要方法。
❌ **禁止**对正常控制流使用异常(系统故障和争用条件除外)。
❌ **禁止**让公共成员根据某些选项决定是否引发异常。
❌ **禁止**让公共成员将异常作为返回值或 `out` 参数返回。
### 1.2 异常引发的代码组织
✔️ 考虑使用**异常生成器方法**Exception Builder
> 从不同位置引发相同异常是常见的。使用辅助方法创建并初始化异常,可避免代码膨胀,并允许引发异常的成员被内联。
```csharp
// 异常生成器方法示例
private static void ThrowArgumentNullException(string paramName)
{
throw new ArgumentNullException(paramName);
}
public void DoSomething(string value)
{
if (value == null)
ThrowArgumentNullException(nameof(value));
// ... 方法逻辑(可被内联)
}
```
❌ **禁止**从异常筛选器块exception filter block中引发异常。
> 筛选器中引发的异常会被 CLR 捕获,导致筛选器返回 false难以调试。
❌ **避免**从 `finally` 块显式引发异常(由方法调用隐式引发的可以接受)。
---
## 二、使用标准异常类型
### 2.1 Exception 和 SystemException
❌ **禁止**引发 `System.Exception``System.SystemException`
❌ **禁止**在框架代码中捕获 `System.Exception``System.SystemException`,除非打算重新引发。
❌ **避免**捕获 `System.Exception``System.SystemException`(顶级异常处理程序除外)。
### 2.2 ApplicationException
❌ **禁止**引发或派生自 `ApplicationException`
### 2.3 InvalidOperationException
✔️ 如果对象处于**不适当的状态**,引发 `InvalidOperationException`
```csharp
// 示例:对象状态不正确
public void Start()
{
if (_isRunning)
throw new InvalidOperationException("服务已在运行中。");
// ...
}
```
### 2.4 ArgumentException 系列
✔️ 如果传递给成员的参数错误,引发 `ArgumentException` 或其子类型之一,优先使用**派生程度最高的异常类型**。
✔️ 引发 `ArgumentException` 子类时,**必须**设置 `ParamName` 属性。
✔️ 使用 `value` 作为属性 setter 隐式值参数的名称。
| 异常类型 | 使用场景 |
|---------|---------|
| `ArgumentException` | 参数值不合法 |
| `ArgumentNullException` | 参数为 null 但不支持 null |
| `ArgumentOutOfRangeException` | 参数值超出有效范围 |
```csharp
// 推荐用法
public void SetAge(int age)
{
if (age < 0 || age > 150)
throw new ArgumentOutOfRangeException(nameof(age), age, "年龄必须在 0-150 之间。");
}
public void SetName(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (name.Length == 0)
throw new ArgumentException("名称不能为空。", nameof(name));
}
```
### 2.5 由 CLR 保留的异常(禁止显式引发)
以下异常仅由 CLR 基础设施引发,**禁止在用户代码中显式引发或捕获**
| 异常类型 | 说明 |
|---------|------|
| `NullReferenceException` | 空引用访问(应通过参数检查避免) |
| `IndexOutOfRangeException` | 数组越界(应通过参数检查避免) |
| `AccessViolationException` | 非法内存访问 |
| `StackOverflowException` | 栈溢出(禁止引发,禁止捕获) |
| `OutOfMemoryException` | 内存不足 |
| `COMException` | COM 互操作异常 |
| `SEHException` | 结构化异常处理 |
| `ExecutionEngineException` | 执行引擎异常 |
> 这些异常指示代码中的 bug 或 CLR 级别的故障,显式引发它们会暴露方法的实现细节。应通过参数检查来预防。
### 2.6 框架内部异常YooInternalException
`YooInternalException` 用于框架内部不变量断言,表示"理论上不可能到达的代码路径"。它与 `ArgumentException` 系列的区别在于**语义和受众**不同:
| 异常类型 | 语义 | 适用场景 |
|---------|------|---------|
| `ArgumentException` 系列 | 调用方传入了错误参数 | 公共 API 的参数校验 |
| `InvalidOperationException` | 对象处于不适当的状态 | 公共 API 的状态校验 |
| `YooInternalException` | 框架内部逻辑错误,理论不可达 | 内部方法的不变量断言 |
✔️ 在 `internal` 方法或框架内部流程中,对理论上不可能发生的状态使用 `YooInternalException`
✔️ 优先使用 `YooInternalException` 而非 `Debug.Assert`,确保在所有构建模式下均能捕获内部逻辑错误。
❌ **禁止**在公共 API 的参数校验中使用 `YooInternalException`,应使用 `ArgumentException` 系列。
---
## 三、异常和性能
### 3.1 性能问题
异常在引发时性能可能慢几个数量级。在严格遵循准则的同时,可通过两种设计模式实现良好性能。
❌ **禁止**因担心异常对性能的影响而使用错误代码。
### 3.2 Tester-Doer 模式(测试者-执行者)
将可能引发异常的成员拆分为两部分:先测试条件,再执行操作。
```csharp
// Tester-Doer 模式
ICollection<int> numbers = ...;
// Tester先检查前置条件
if (!numbers.IsReadOnly)
{
// Doer再执行操作
numbers.Add(1);
}
```
| 角色 | 说明 | 示例 |
|------|------|------|
| **Tester测试者** | 检查前置条件是否满足 | `IsReadOnly``CanSeek``ContainsKey` |
| **Doer执行者** | 执行可能引发异常的操作 | `Add``Seek``GetValue` |
✔️ 考虑对在**常见场景中可能引发异常**的成员使用 Tester-Doer 模式。
### 3.3 Try-Parse 模式(尝试-分析)
对于**极其性能敏感**的 API使用比 Tester-Doer 更快的 Try-Parse 模式。
```csharp
// Try-Parse 模式
public struct DateTime
{
// 失败时引发异常
public static DateTime Parse(string dateTime) { ... }
// 失败时返回 false成功通过 out 参数返回结果
public static bool TryParse(string dateTime, out DateTime result) { ... }
}
// 使用方式
if (DateTime.TryParse(input, out var date))
{
// 解析成功
}
else
{
// 解析失败,无异常开销
}
```
✔️ 对在常见场景中可能引发异常的成员使用 Try-Parse 模式。
✔️ 方法命名使用 **`Try` 前缀**和 **`bool` 返回类型**。
✔️ 为每个 Try-Parse 成员提供一个对应的**异常引发版本**。
> 例如 `Parse` / `TryParse`、`GetValue` / `TryGetValue`。
---
## 四、速查表
### 4.1 何时引发异常
| 场景 | 行为 |
|------|------|
| 成员无法完成其设计用途 | 引发异常 |
| 参数为 null不支持时 | 引发 `ArgumentNullException` |
| 参数超出有效范围 | 引发 `ArgumentOutOfRangeException` |
| 参数值不合法 | 引发 `ArgumentException` |
| 对象状态不正确 | 引发 `InvalidOperationException` |
| 操作不被支持 | 引发 `NotSupportedException` |
| 进一步执行不安全 | 调用 `Environment.FailFast` |
### 4.2 禁止做的事情
| 禁止 | 原因 |
|------|------|
| 返回错误代码 | 异常是唯一的错误报告方式 |
| 用异常做控制流 | 异常仅用于异常情况 |
| 引发 `Exception` / `SystemException` | 过于笼统 |
| 引发 CLR 保留异常 | 由运行时专用 |
| 在 finally 中显式引发 | 可能掩盖原始异常 |
| 在异常筛选器中引发 | 难以调试 |
### 4.3 性能优化模式选择
| 模式 | 适用场景 | 示例 |
|------|---------|------|
| **Tester-Doer** | 常见场景中的条件检查 | `if (!IsReadOnly) Add(item)` |
| **Try-Parse** | 极其性能敏感的 API | `TryParse(input, out result)` |

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: d10f50d328fa47f44ad92a7b89f3d757
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,335 +0,0 @@
# .NET 设计模式准则
> 参考来源:《框架设计准则:可重用 .NET 库的约定、惯例和模式第3版》第9章
>
> 核心原则:**框架设计者应提供完整的端到端解决方案,而非仅提供 API。高级 API 应当"开箱即用",用户无需了解底层复杂性。**
---
## 一、聚合组件设计
聚合组件将多个底层分解类型Factored Types绑定到一个更高级别的组件中为常见场景提供简化入口。它是开发者探索其命名空间的**起点**。
### 1.1 Create-Set-Call 使用模式
所有聚合组件都应支持此模式:
```csharp
// 1. Create使用默认或简单构造函数创建
var queue = new MessageQueue();
// 2. Set设置属性
queue.Path = @".\private$\orders";
// 3. Call调用方法
queue.Send("Hello World");
```
### 1.2 聚合组件 vs 分解类型
| 特征 | 聚合组件 | 分解类型 |
|------|---------|---------|
| **目的** | 简化常见场景 | 提供精细控制 |
| **状态** | 可以有模式和临时无效状态 | 不应有模式,生命周期清晰 |
| **构造** | 默认/简单构造函数 | 可能需要复杂初始化 |
| **可用性优先级** | 最高 | 较低 |
| **示例** | `SerialPort``MessageQueue` | `Stream``GZipStream` |
```csharp
// 聚合组件暴露内部分解类型以支持高级场景
var port = new SerialPort("COM1");
port.Open();
GZipStream compressed = new GZipStream(port.BaseStream, CompressionMode.Compress);
compressed.Write(data, 0, data.Length);
port.Close();
```
### 1.3 设计准则
✔️ 考虑为常用功能区域提供聚合组件。
✔️ 设计聚合组件以便**极简初始化后即可使用**。如果需要初始化,未初始化时引发的异常应清楚说明需要做什么。
✔️ **必须**支持 Create-Set-Call 使用模式。
✔️ 为所有构造函数参数提供对应的可读写属性,确保始终可以用默认构造函数 + 属性设置代替参数化构造函数。
✔️ 在聚合组件中使用**事件**代替委托 API 和需要重写的虚拟成员。
✔️ 选择最具"吸引力"的名称给聚合组件(如 `File`),将较不直观的名称留给分解类型(如 `StreamReader`)。
❌ **禁止**在常见场景中要求用户继承、重写方法或实现接口。
❌ **禁止**在常见场景中要求用户做编写代码以外的事(如配置文件、生成资源文件等)。
❌ **禁止**将分解类型设计为有模式的,它们应有明确的生命周期。
✔️ 当用户在无效状态下调用方法时,抛出 `InvalidOperationException`,异常消息应清楚说明需要更改哪些属性。
---
## 二、异步编程准则
### 2.1 异步方法返回类型
✔️ 考虑在方法**经常同步完成**时返回 `ValueTask<T>`
> 例如 `Stream.ReadAsync` 由于缓冲区中常有可用数据,经常同步完成。使用 `ValueTask<T>` 避免了每次调用都分配 `Task<T>` 对象的开销。
```csharp
// ValueTask<T> 适用于经常同步完成的场景
public virtual ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default);
```
> 注意:当方法确实异步完成时,`ValueTask<T>` 仍然需要创建 `Task` 实例。对于始终异步完成的操作,`ValueTask<T>` 会带来轻微的性能损失和可用性降低,此时应使用 `Task<T>`。
### 2.2 ConfigureAwait
✔️ 在 async 方法中等待异步操作时,使用 `await task.ConfigureAwait(false)`,除非在依赖同步上下文的应用模型中。
> 默认情况下,.NET 使用 `SynchronizationContext.Current` 决定如何继续处理 Task。控制台应用中Task 完成回调在后台线程上执行。WinForms/WPF 中,默认将所有回调调度到 UI 线程。`ConfigureAwait(false)` 避免了不必要的线程切换。
### 2.3 避免死锁
❌ **禁止**在 async 方法中调用 `Task.Wait()` 或读取 `Task.Result` 属性,应使用 `await`
> `await` 会让出当前 Task允许其他 Task 继续。`Task.Wait()` 和 `Task.Result` 执行阻塞式同步等待,效率低下且在某些场景下会导致死锁。
✔️ 在 async 方法的实现中,调用**异步方法变体**而非同步变体。
### 2.4 CancellationToken 取消准则
✔️ 因 `CancellationToken` 取消工作时,抛出 `OperationCanceledException`
```csharp
// 两种等效写法
if (cancellationToken.IsCancellationRequested)
throw new OperationCanceledException(cancellationToken);
// 或者
cancellationToken.ThrowIfCancellationRequested();
```
### 2.5 异步方法中的异常
✔️ 将用法错误(如参数验证)的异常直接从非 async 包装方法中抛出,以提高可调试性。
```csharp
// 正确:异常直接抛出,不包装在 Task 中
public Task SaveAsync(string filename)
{
if (filename == null)
throw new ArgumentNullException(nameof(filename));
return SaveAsyncCore(filename);
}
private async Task SaveAsyncCore(string filename)
{
// 实际异步工作...
}
```
```csharp
// 错误:异常在 Task 内部抛出,调用栈不清晰
public async Task SaveAsync(string filename)
{
if (filename == null)
throw new ArgumentNullException(nameof(filename));
// ...
}
```
### 2.6 IAsyncEnumerable\<T\>
✔️ 在使用 `yield return``IAsyncEnumerable` 方法中,为 `CancellationToken` 参数添加 `[EnumeratorCancellation]` 特性。
> 不添加此特性时,`GetAsyncEnumerator` 传入的 `CancellationToken` 值会被编译器生成的实现忽略。
```csharp
public static async IAsyncEnumerable<int> GenerateValues(
int start, int count,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
int end = start + count;
for (int i = start; i < end; i++)
{
await Task.Delay(i, cancellationToken).ConfigureAwait(false);
yield return i;
}
}
```
✔️ 对 `IAsyncEnumerable` 参数使用 `await foreach` 时,使用 `WithCancellation` 传递 `CancellationToken`
```csharp
await foreach (int value in source.WithCancellation(cancellationToken).ConfigureAwait(false))
{
// ...
}
```
---
## 三、Dispose 模式与 IAsyncDisposable
### 3.1 基本 Dispose 模式
托管内存只是系统资源的一种。文件句柄、网络连接、数据库连接等非托管资源仍需显式释放。
终结器Finalizer的缺点
- GC 在**不确定的时间**才调用终结器,延迟可能不可接受
- 需要终结的对象的内存回收会被**推迟到下一轮 GC**
✔️ 考虑在本身不持有非托管资源的类上实现基本 Dispose 模式,如果其**子类型可能持有**。
> 例如 `System.IO.Stream` 是一个不持有资源的抽象基类,但其大多数子类持有资源。
✔️ 在对象被释放后,从任何不能使用的成员中抛出 `ObjectDisposedException`
✔️ 考虑提供 `Close()` 方法作为 `Dispose()` 的补充(如果"close"是该领域的标准术语)。
### 3.2 作用域操作
✔️ 考虑返回一个 disposable 值,代替让调用者手动配对 "begin" 和 "end" 方法。
```csharp
// 使用 disposable 作用域代替 begin/end 配对
using (var scope = logger.BeginScope("Processing"))
{
// 作用域内的操作...
} // 自动结束作用域
```
### 3.3 IAsyncDisposable
`IAsyncDisposable` 允许在 `Dispose()` 涉及 I/O 或其他阻塞操作时使用异步方法进行资源清理。
```csharp
public partial class SomeType : IDisposable, IAsyncDisposable
{
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) { /* ... */ }
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore();
Dispose(false);
GC.SuppressFinalize(this);
}
protected virtual ValueTask DisposeAsyncCore() { /* ... */ }
}
```
> 同时实现 `IDisposable` 和 `IAsyncDisposable` 时,`DisposeAsync` 调用 `Dispose(false)` 以避免同步释放路径重复执行。
---
## 四、Span / Memory 缓冲区操作
### 4.1 Span\<T\> 和 ReadOnlySpan\<T\>
`Span<T>` 表示来自托管数组、stackalloc 数组、指针或字符串的连续内存,支持 O(1) 切片操作。
✔️ 考虑使用 `Span<T>` 作为缓冲区的表示。
✔️ 尽可能使用 `ReadOnlySpan<T>` 代替 `Span<T>`
❌ **避免**返回 Span 或 ReadOnlySpan除非返回值的生命周期非常明确。
✔️ 对返回的、非来自调用方的 Span 提供**非常清晰的所有权规则文档**。
✔️ 考虑从 get-only 属性或无参方法返回 `ReadOnlySpan<T>` 来表示固定数据(当 T 是不可变类型时)。
✔️ 考虑返回 `System.Range`(表示 Span 参数的边界)而非参数的切片。
> `Span<T>` 可以转换为 `ReadOnlySpan<T>`,但反过来不行。如果方法接受 `ReadOnlySpan` 并返回切片,调用方无法轻松确定其可写 Span 的等效切片。
### 4.2 Memory\<T\> 和 ReadOnlyMemory\<T\>
`Span<T>``ref struct`,不能在异步方法中使用。需要存储缓冲区时应使用 `Memory<T>`
✔️ 在异步方法中使用 `ReadOnlyMemory<T>` 代替 `ReadOnlySpan<T>`
✔️ 在异步方法中使用 `Memory<T>` 代替 `Span<T>`
✔️ 当构造函数或方法的目的是**保存缓冲区引用**时,使用 `Memory<T>` / `ReadOnlyMemory<T>` 作为参数。
### 4.3 命名和重载
✔️ 对接受输出缓冲区和单个输入缓冲区的方法,输入缓冲区参数命名为 **`source`**,输出缓冲区参数命名为 **`destination`**。
❌ **避免**在 `Span<T>``ReadOnlySpan<T>` 之间,或 `Memory<T>``ReadOnlyMemory<T>` 之间重载同一方法。
> 如果一个方法以只读方式操作缓冲区,另一个以读写方式操作,它们应有不同的方法名称。
---
## 五、工厂模式
工厂是抽象对象创建过程的操作或操作集合,允许专用语义和更细粒度的实例化控制。
### 设计准则
✔️ **优先使用构造函数**而非工厂,因为构造函数通常更可用、一致和方便。
✔️ 当工厂方法声明在独立的工厂类型上时,考虑使用 **`Create` + 类型名称** 命名。
> 例如创建按钮的工厂方法命名为 `CreateButton`。在某些情况下可使用领域特定名称,如 `File.Open`。
✔️ 考虑使用 **类型名称 + `Factory`** 命名工厂类型。
> 例如创建 `Control` 对象的工厂类型命名为 `ControlFactory`。
---
## 六、超时处理
✔️ **优先使用方法参数**作为用户指定超时时间的机制。
```csharp
server.PerformOperation(timeout);
```
> 备选方案是使用属性:
```csharp
server.Timeout = timeout;
server.PerformOperation();
```
✔️ 优先使用 `TimeSpan` 表示超时时间。
✔️ 在 Unity 项目中,可以使用 `long` 毫秒数代替 `TimeSpan` 表示超时或时间切片,以避免值类型转换开销。
✔️ 超时到期时抛出 `System.TimeoutException`
❌ **禁止**使用错误代码指示超时到期。
---
## 七、LINQ 支持
✔️ 实现 `IEnumerable<T>` 以启用基本 LINQ 支持。
```csharp
public class RangeOfInt32s : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator() { /* ... */ }
IEnumerator IEnumerable.GetEnumerator() { /* ... */ }
}
// 实现 IEnumerable<T> 后即可使用 LINQ
var result = new RangeOfInt32s().Where(x => x > 10);
```
✔️ 考虑实现 `ICollection<T>` 以提升查询运算符的性能。
✔️ 将查询扩展方法放在主命名空间的 **`Linq` 子命名空间**中。
> 例如 `System.Data` 功能的扩展方法应放在 `System.Data.Linq` 命名空间中。

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 1ddd0c64da4a16244bf65c56c9d403b2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,390 +0,0 @@
# .NET 使用指南
> 参考来源:[Microsoft .NET 框架设计准则 - 使用指南](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/usage-guidelines)
>
> 核心原则:**在公开 API 中正确使用内置框架类型,优先选择抽象和接口而非具体实现类型。**
---
## 一、数组
✔️ 在公共 API 中优先使用**集合**而不是数组。
✔️ 考虑使用**交错数组**`int[][]`)而不是多维数组(`int[,]`)。
> 交错数组的元素大小可以不同,浪费空间更少。此外 CLR 优化了交错数组的索引操作,在某些场景中性能更好。
❌ **禁止**使用只读数组字段。
> 字段本身是只读的不能被替换,但数组中的元素仍可被修改。
```csharp
// 错误:数组元素仍可被外部修改
public static readonly int[] InvalidValues = { 1, 2, 3 };
// 正确:使用 ReadOnlyCollection 或 ImmutableArray
public static readonly ReadOnlyCollection<int> InvalidValues =
new ReadOnlyCollection<int>(new[] { 1, 2, 3 });
```
---
## 二、特性Attribute
特性是可添加到程序集、类型、成员和参数的元数据注释,通过反射 API 在运行时访问。
### 设计准则
✔️ 使用 `Attribute` 后缀命名自定义特性类。
✔️ **必须**对自定义特性应用 `[AttributeUsage]`
✔️ 为**必需参数**提供只读属性和对应的构造函数参数。
✔️ 为**可选参数**提供可读写属性(通过 setter 设置)。
✔️ 如果可能,请**密封**自定义特性类(使查找速度更快)。
❌ **避免**重载自定义特性的构造函数(单一构造函数更清晰)。
❌ **避免**提供构造函数参数来初始化可选属性(不要同时支持构造函数和 setter 两种方式设置同一属性)。
```csharp
// 推荐设计
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class MyCustomAttribute : Attribute
{
// 必需参数:通过构造函数 + 只读属性
public MyCustomAttribute(string name)
{
Name = name;
}
public string Name { get; }
// 可选参数:通过可读写属性
public int Priority { get; set; }
}
// 使用方式
[MyCustom("example", Priority = 5)]
public class MyClass { }
```
---
## 三、集合
### 3.1 公共 API 中的集合选择
❌ **禁止**在公共 API 中使用弱类型集合。
> 所有表示集合项的返回值和参数都应使用确切的项类型。
❌ **禁止**在公共 API 中使用 `ArrayList``List<T>`
> 这些是为内部实现设计的数据结构。`List<T>` 牺牲了 API 简洁性和灵活性来优化性能。
❌ **禁止**在公共 API 中使用 `Hashtable``Dictionary<TKey,TValue>`
> 应使用 `IDictionary`、`IDictionary<TKey,TValue>` 或实现这些接口的自定义类型。
❌ **禁止**在同一类型上同时实现 `IEnumerator<T>``IEnumerable<T>`
❌ **禁止**从非 `GetEnumerator` 方法返回枚举器类型。
### 3.2 集合参数
✔️ 尽可能使用**最不专用的类型**作为参数类型。
> 大多数以集合为参数的成员应使用 `IEnumerable<T>` 接口。
❌ **避免**仅为访问 `Count` 属性而使用 `ICollection<T>` 作为参数。
> 应使用 `IEnumerable<T>`,并动态检查是否实现了 `ICollection<T>`。
### 3.3 集合属性和返回值
❌ **禁止**提供可设置的集合属性。
> 用户可以通过先清除再添加来替换内容。如需替换整个集合,提供 `AddRange` 方法。
✔️ 对读写集合的属性或返回值使用 `Collection<T>` 或其子类。
✔️ 对只读集合使用 `ReadOnlyCollection<T>` 或其子类。
❌ **禁止**从集合属性或返回集合的方法中返回 `null`
> **应返回空集合或空数组**。一般规则null 和空集合应被视为相同。
### 3.4 快照集合 vs 实时集合
| 类型 | 说明 | 示例 |
|------|------|------|
| **快照集合** | 表示某个时间点的状态 | 数据库查询结果 |
| **实时集合** | 始终表示当前状态 | ComboBox 的 Items |
❌ **禁止**从属性返回快照集合getter 应轻量,快照需要 O(n) 复制)。
✔️ 使用快照集合或实时 `IEnumerable<T>` 表示可变集合(无需显式修改即可变化的集合)。
### 3.5 数组 vs 集合选择
✔️ 在大多数情况下**优先选择集合**——更好的内容控制、更易使用。
✔️ 在**低级 API** 中考虑使用数组以最小化内存消耗和最大化性能。
✔️ 使用**字节数组**`byte[]`)而非字节集合。
❌ 如果每次调用属性 getter 都需要返回新数组(如内部数组的副本),**禁止**对属性使用数组。
### 3.6 自定义集合设计
✔️ 设计新集合时考虑继承自 `Collection<T>``ReadOnlyCollection<T>``KeyedCollection<TKey,TItem>`
✔️ **必须**实现 `IEnumerable<T>`,按需考虑实现 `ICollection<T>``IList<T>`
✔️ 如果集合项具有唯一键,考虑使用**键控集合**(继承自 `KeyedCollection<TKey,TItem>`)。
❌ **禁止**继承自非泛型基集合(如 `CollectionBase`)。
❌ **避免**在与集合概念无关的复杂 API 类型上实现集合接口。
### 3.7 自定义集合命名
✔️ 实现 `IDictionary` / `IDictionary<TKey,TValue>` 的类型使用 **`Dictionary`** 后缀。
✔️ 实现 `IEnumerable` 且表示项列表的类型使用 **`Collection`** 后缀。
✔️ 考虑用项类型名称作为集合名称前缀:`AddressCollection`
✔️ 只读集合考虑使用 **`ReadOnly`** 前缀:`ReadOnlyStringCollection`
❌ **避免**使用表示具体实现的后缀(如 `LinkedList``Hashtable`)。
---
## 四、序列化
序列化是将对象转换为可持久保存或传输格式的过程。
### 4.1 三种序列化技术
| 技术 | 主要类型 | 适用场景 |
|------|---------|---------|
| **数据协定序列化** | `[DataContract]` / `[DataMember]` / `DataContractSerializer` | 一般持久性、Web 服务、JSON |
| **XML 序列化** | `XmlSerializer` | 需要完全控制 XML 形状 |
| **运行时序列化** | `[Serializable]` / `ISerializable` / `BinaryFormatter` | .NET 远程处理 |
### 4.2 选择准则
✔️ 设计新类型时,考虑序列化需求。
✔️ 如果类型需要持久保存或用于 Web 服务,考虑支持**数据协定序列化**。
✔️ 如果需要完全控制 XML 形状,考虑支持 **XML 序列化**
✔️ 如果类型需要跨 .NET 远程处理边界传输,考虑支持**运行时序列化**。
❌ **避免**仅出于一般持久性原因支持运行时序列化或 XML 序列化,应优先使用数据协定序列化。
### 4.3 数据协定序列化准则
✔️ 如果类型可在部分信任中使用,考虑将数据成员标记为 `public`
✔️ 具有 `[DataMember]` 的属性**必须**同时具有 getter 和 setter。
✔️ 考虑使用序列化回调初始化反序列化后的实例(反序列化时不调用构造函数)。
> 使用 `[OnDeserialized]`、`[OnDeserializing]`、`[OnSerializing]`、`[OnSerialized]` 特性。
✔️ 考虑使用 `[KnownType]` 指示反序列化复杂对象图时应使用的具体类型。
✔️ 创建或更改可序列化类型时,考虑**向后和向前兼容性**。
✔️ 考虑实现 `IExtensibleDataObject` 以允许不同版本间的往返序列化。
### 4.4 XML 序列化准则
❌ **避免**专门为 XML 序列化设计类型(已被数据协定序列化取代)。
✔️ 如果需要精细控制 XML 形状,考虑实现 `IXmlSerializable` 接口。
### 4.5 运行时序列化准则
✔️ 如果需要完全控制序列化过程,实现 `ISerializable` 并提供序列化构造函数。
✔️ 序列化构造函数应为 `protected`
✔️ **必须**显式实现 `ISerializable` 成员。
```csharp
[Serializable]
public class Person : ISerializable
{
public string Name { get; set; }
public Person() { }
// 序列化构造函数protected
protected Person(SerializationInfo info, StreamingContext context)
{
Name = info.GetString(nameof(Name));
}
// 显式实现
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue(nameof(Name), Name);
}
}
```
---
## 五、System.Xml 用法
❌ **禁止**使用 `XmlNode``XmlDocument` 表示 XML 数据。
✔️ 使用 `XmlReader``XmlWriter``IXPathNavigable``XNode` 子类型作为接受或返回 XML 的成员的输入输出。
> 使用抽象类型可以将方法与内存中 XML 文档的具体实现分离,允许与虚拟 XML 数据源一起工作。
❌ 如果要创建表示基础对象模型的 XML 视图,**禁止**继承 `XmlDocument`
---
## 六、相等运算符
### 6.1 通用准则
❌ **禁止**只重载 `==``!=` 中的一个而忽略另一个(必须成对重载)。
✔️ 确保 `Object.Equals` 和相等运算符具有**相同的语义和相似的性能**。
> 重载相等运算符时,通常需要同时重写 `Object.Equals`。
❌ **避免**从相等运算符引发异常(参数为 null 时应返回 false
### 6.2 值类型的相等运算符
✔️ 如果相等有意义,**应该**对值类型重载相等运算符。
> 大多数语言没有为值类型提供默认的 `operator==` 实现。
### 6.3 引用类型的相等运算符
❌ **避免**在**可变引用类型**上重载相等运算符。
> 内置运算符实现引用相等性,改为值相等性会使许多开发人员困惑。不可变引用类型则问题较小。
❌ **避免**对引用类型重载相等运算符(如果实现速度远低于引用相等性)。
---
## 七、IEquatable / IComparable 准则
`IComparable<T>` 定义排序语义(小于、等于、大于),主要用于排序。`IEquatable<T>` 定义相等语义,主要用于查找。
✔️ **必须**在值类型上实现 `IEquatable<T>`
> `Object.Equals` 在值类型上会导致装箱,且默认实现使用反射,效率低下。
✔️ 实现 `IEquatable<T>` 时**必须**同时重写 `Object.Equals`
✔️ 实现 `IEquatable<T>` 时考虑重载 `operator==``operator!=`
✔️ 实现 `IComparable<T>` 时**必须**同时实现 `IEquatable<T>`
✔️ 重写 `Object.Equals` 时**必须**同时重写 `GetHashCode`
---
## 八、Object 准则
### 8.1 Object.Equals
- **值类型**默认实现:当所有字段相等时返回 true"值相等"),但实现**使用反射**,通常效率低下,**应当重写**
- **引用类型**默认实现:当两个引用指向同一对象时返回 true"引用相等"
❌ **禁止**在**可变引用类型**上实现值相等。
> 实现值相等的引用类型(如 `System.String`)应当是不可变的。
### 8.2 Object.GetHashCode
✔️ 重写 `Equals` 时**必须**重写 `GetHashCode`,确保被视为相等的两个对象具有相同的哈希码。
✔️ 尽力确保 `GetHashCode` 为类型的所有对象生成**均匀分布**的数字,以最小化哈希表冲突。
### 8.3 Object.ToString
`Object.ToString` 用于通用显示和调试目的,默认实现仅返回类型名称。
✔️ 建议**始终重写** `ToString`,提供有意义的字符串表示。
---
## 九、Nullable\<T\> 准则
❌ **禁止**使用 `Nullable<T>` 表示可选参数。
> 应使用方法重载来表示可选参数。
```csharp
// 正确:使用重载
public class Foo
{
public Foo(string name, int id) { }
public Foo(string name) { }
}
```
❌ **避免**使用 `Nullable<bool>` 表示通用三态值。
> `Nullable<bool>` 仅应用于真正可选的布尔值true、false、不可用。如果需要表示三种状态如 yes、no、cancel应使用枚举。
---
## 十、URI 准则
✔️ 使用 `System.Uri` 表示 URI 和 URL 数据,而非普通字符串。
> `System.Uri` 提供了更安全、更丰富的 URI 表示方式。使用普通字符串大量操作 URI 相关数据已被证明会导致许多安全和正确性问题。
---
## 十一、ICloneable 准则
❌ **禁止**在公共 API 中使用 `ICloneable`
> `ICloneable` 的问题在于协定未指定克隆是浅拷贝还是深拷贝——有些实现返回浅拷贝,有些返回深拷贝。调用者永远无法确定会得到什么,这使得该接口毫无用处。
✔️ 如果需要克隆机制,考虑在类型上定义自己的 `Clone` 方法,并明确文档说明是浅拷贝还是深拷贝。
---
## 十二、速查表
### 公共 API 中的类型选择
| 场景 | 推荐 | 避免 |
|------|------|------|
| 集合参数 | `IEnumerable<T>` | `List<T>``ArrayList` |
| 读写集合返回值 | `Collection<T>` 或子类 | `List<T>` |
| 只读集合返回值 | `ReadOnlyCollection<T>` | 数组(需复制) |
| 字典参数/返回值 | `IDictionary<TKey,TValue>` | `Dictionary<TKey,TValue>` |
| 表示 XML 数据 | `XmlReader``XNode` | `XmlNode``XmlDocument` |
| 序列化 | 数据协定序列化 | 运行时序列化(除非远程处理) |
| 低级高性能 API | 数组 | 集合 |
| 字节数据 | `byte[]` | `Collection<byte>` |
### 禁止事项
| 禁止 | 原因 |
|------|------|
| 公共 API 返回 `null` 集合 | 应返回空集合或空数组 |
| 可设置的集合属性 | 用户应通过 Clear+Add 替换内容 |
| 只读数组字段 | 元素仍可被修改 |
| 属性返回快照集合 | getter 应轻量,不应有 O(n) 开销 |
| 继承 `CollectionBase` | 应使用泛型基类 |

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 95ca2d569cd5c124892e8f6e0a01efea
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,225 +0,0 @@
# .NET 日志规范
> 参考来源:[Microsoft .NET 框架设计准则 - 异常消息](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/exceptions-and-performance) · [.NET Runtime 源码日志风格](https://github.com/dotnet/runtime)
>
> 核心原则:**措辞简洁准确、格式统一规范、使用地道英语、避免泄露敏感信息**
---
## 一、变量引用规则
### 1.1 字符串变量值加单引号
文件路径、名称、GUID、标识符等字符串变量值用单引号包裹便于在日志中区分变量边界。
```csharp
// ✔️
$"File cache entry not found: '{bundleGuid}'."
$"Invalid clear method: '{options.ClearMethod}'."
$"Could not find file '{filePath}'."
// ❌
$"File cache entry not found: {bundleGuid}."
```
### 1.2 数值变量不加引号
数值类型的变量(整数、浮点、枚举值)不加引号。
```csharp
// ✔️
$"MaxTimeSlice must be at least {MinTimeSlice} ms."
$"Does not support bundle type: {options.Bundle.BundleType}."
// ❌
$"Does not support bundle type: '{options.Bundle.BundleType}'."
```
### 1.3 类型名作句首主语不加引号
类型名(通常通过 `nameof()``GetType().Name` 获取)做句子主语时,不加引号。
```csharp
// ✔️
$"{nameof(SandboxBundleCache)} does not support synchronous loading."
$"AsyncOperationSystem is not initialized."
// ❌
$"'{nameof(SandboxBundleCache)}' does not support synchronous loading."
```
### 1.4 异常对象不加引号
异常对象(`{ex}``{ex.Message}`)直接拼接,不加引号。
```csharp
// ✔️
YooLogger.Error($"File verification exception: {ex.Message}.");
YooLogger.Warning($"Failed to delete cache bundle folder: {ex}.");
// ❌
YooLogger.Error($"File verification exception: '{ex.Message}'.");
```
---
## 二、消息格式规则
### 2.1 首字母大写,末尾加句号
所有日志和异常消息使用 Sentence case首字母大写末尾加句号。
```csharp
// ✔️
SetError("Unity engine load failed.");
throw new Exception("Catalog file data is null or empty.");
// ❌
SetError("unity engine load failed");
throw new Exception("catalog file data is null or empty");
```
### 2.2 使用完整句子
使用语法完整的句子,不省略主语或谓语。
> **来源**.NET 官方规范 — *"Do use complete sentences. 'Binding is too long.' rather than 'Binding too long.'"*
```csharp
// ✔️
"Decryptor is null."
"Loaded bundle handle is null."
// ❌
"Decryptor null."
"Bundle handle null."
```
> **例外**`not found` 等 .NET 生态中广泛使用的惯用短语,允许省略 `was`,以保持简洁和一致性。
```csharp
// ✔️ 惯用短语,省略 was
"File cache entry not found: '{bundleGuid}'."
"Asset not found: '{assetPath}'."
// ❌ 冗余
"File cache entry was not found: '{bundleGuid}'."
```
### 2.3 不要以冠词或变量开头
消息以关键实体词开头,不要以 `The` / `A` / `An` 或变量开头,便于日志搜索和排序。
> **来源**.NET 官方规范 — *"Do not start with an article or variable. 'Log file {0} is full.' is preferable to '{0} log file is full.'"*
```csharp
// ✔️
$"Cache entry already exists: '{bundleGuid}'."
$"Log file '{name}' is full."
// ❌
$"The cache entry already exists: '{bundleGuid}'."
$"'{name}' log file is full."
```
### 2.4 不要使用感叹号和问号
消息末尾只使用句号,禁止感叹号和问号。
> **来源**.NET 官方规范 — *"Do not use question marks or exclamation points."*
```csharp
// ✔️
"Command is unrecognizable."
// ❌
"Command is unrecognizable!!"
"Did the operation complete?"
```
---
## 三、语气和措辞规则
### 3.1 使用中性语气
使用中性客观的措辞,不归咎用户,不拟人化程序。
> **来源**.NET 官方规范 — *"Do use a neutral tone."* / *"Do not personify."*
```csharp
// ✔️
"Command is unrecognizable."
"Node parameter cannot use this protocol."
// ❌
"Bad input."
"Parameter node does not speak any of our protocols."
```
### 3.2 省略 `this.` 前缀
`this.GetType().Name` 中的 `this.` 可省略。
```csharp
// ✔️
$"Exception in {GetType().Name}.InternalStart: {ex}."
// ❌
$"Exception in {this.GetType().Name}.InternalStart: {ex}."
```
### 3.3 不要泄露敏感信息
异常和日志消息中避免包含密钥、密码、完整用户路径等敏感信息。
> **来源**.NET 官方规范 — *"Do not disclose sensitive information in exception messages."*
---
## 四、一致性规则
### 4.1 保持一致性
同一项目中相同语义使用相同句式,避免同一错误用不同表达方式。
```csharp
// ✔️ 统一格式
$"File cache entry not found: '{bundleGuid}'." // 所有缓存未命中统一使用此句式
$"Exception in {GetType().Name}.{MethodName}: {ex}." // 所有异常日志统一使用此句式
// ❌ 同一语义不同表达
"Cached bundle not found." // ← 与上面的 "File cache entry not found" 不一致
```
### 4.2 避免中式英语和语法错误
使用地道的英语表达,避免中文直译造成的不自然句式。
| 中式英语 | 地道表达 | 说明 |
|----------|----------|------|
| `"cannot match the file hash"` | `"File hash does not match."` | 主语应是名词而非动词短语 |
| `"Failed fallback load bundle"` | `"Failed to load bundle with fallback."` | 缺少介词和完整句法 |
| `"Not found xxx in yyy"` | `"Could not find xxx in yyy."` | `Not found` 不是完整句子 |
| `"The bundle file is lose"` | `"The bundle file is missing."` | 词性错误lose → missing |
| `"Begin to download"` | `"Starting download."` | `begin to` 是典型中式英语 |
---
## 五、速查表
| # | 规则 | 正确示例 | 错误示例 |
|---|------|---------|---------|
| 1 | 字符串变量值加单引号 | `'{bundleGuid}'` | `{bundleGuid}` |
| 2 | 数值变量不加引号 | `{count}` | `'{count}'` |
| 3 | 类型名作主语不加引号 | `TypeName is not ...` | `'TypeName' is not ...` |
| 4 | 异常对象不加引号 | `{ex.Message}` | `'{ex.Message}'` |
| 5 | 首字母大写,末尾加句号 | `"Load failed."` | `"load failed"` |
| 6 | 使用完整句子 | `"Decryptor is null."` | `"Decryptor null."` |
| 7 | 不以冠词或变量开头 | `"Cache entry ..."` | `"The cache entry ..."` |
| 8 | 只用句号 | `"Failed."` | `"Failed!!"` |
| 9 | 省略 `this.` | `GetType().Name` | `this.GetType().Name` |
| 10 | 使用中性语气 | `"Command is unrecognizable."` | `"Bad input."` |
| 11 | 不泄露敏感信息 | — | 密钥、密码、用户路径 |
| 12 | 保持一致性 | 同语义同句式 | 同错误不同表达 |
| 13 | 避免中式英语 | `"File hash does not match."` | `"cannot match the file hash"` |

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: a8f2f1e57843e104f95aa6ad5ad3e1a9
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,531 +0,0 @@
# .NET 注释规范
> 参考来源:[Microsoft C# XML 文档注释](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/xmldoc/) · [推荐的文档注释标签](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/xmldoc/recommended-tags) · [框架设计准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/)
>
> 核心原则:**公共 API 必须注释、说明意图而非复述代码、保持与代码同步**
---
## 一、概述
C# 使用 XML 文档注释为代码提供结构化文档。编译器从 `///`(三斜杠)注释中生成 XML 文件,供 IDE 智能提示和文档生成工具使用。
---
## 二、XML 注释标签
### 2.1 核心标签
| 标签 | 用途 | 适用场景 |
|------|------|----------|
| `<summary>` | 简要描述类型或成员的功能 | 所有公共类型和成员 |
| `<param>` | 描述方法参数 | 方法、构造函数 |
| `<returns>` | 描述方法返回值 | 有返回值的方法 |
| `<remarks>` | 补充说明,提供额外细节 | 需要详细解释的成员 |
| `<value>` | 描述属性所代表的值 | 属性 |
### 2.2 引用标签
| 标签 | 用途 | 示例 |
|------|------|------|
| `<see cref=""/>` | 行内引用其他类型或成员 | `<see cref="MyClass"/>` |
| `<seealso cref=""/>` | 在"另请参阅"区域添加引用 | `<seealso cref="OtherMethod"/>` |
| `<paramref name=""/>` | 引用参数名 | `<paramref name="count"/>` |
| `<typeparamref name=""/>` | 引用泛型类型参数名 | `<typeparamref name="T"/>` |
### 2.3 格式标签
| 标签 | 用途 |
|------|------|
| `<para>` | 在 `<remarks>` 等标签内分段 |
| `<list>` | 创建列表或表格 |
| `<code>` | 多行代码示例 |
| `<example>` | 包含使用示例 |
### 2.4 泛型与继承标签
| 标签 | 用途 |
|------|------|
| `<typeparam>` | 描述泛型类型参数 |
| `<inheritdoc/>` | 继承基类或接口的文档注释 |
---
## 三、注释范围要求
### 3.1 必须添加注释的成员
| 访问级别 | 要求 |
|----------|------|
| `public` | **必须**添加完整 XML 注释 |
| `protected` | **必须**添加完整 XML 注释 |
| `internal` | **建议**添加 XML 注释 |
| `private` | 仅在逻辑复杂时添加 |
### 3.2 各成员类型的注释要求
| 成员类型 | 必需标签 | 可选标签 |
|----------|---------|---------|
| 类 / 结构 | `<summary>` | `<remarks>``<typeparam>` |
| 接口 | `<summary>` | `<remarks>` |
| 方法 | `<summary>``<param>``<returns>` | `<remarks>``<example>` |
| 构造函数 | `<summary>``<param>` | — |
| 属性 | `<summary>` | `<value>` |
| 事件 | `<summary>` | `<remarks>` |
| 枚举类型 | `<summary>` | — |
| 枚举值 | `<summary>` | — |
| 委托 | `<summary>``<param>``<returns>` | — |
| 接口实现成员 | `<inheritdoc/>` | — |
| override 方法 | **不添加**注释 | — |
---
## 四、书写原则
### 4.1 `<summary>` 书写规范
✔️ **一句话**说明"做什么",而非"怎么做"。
✔️ 以**第三人称动词**开头。
✔️ 句号规则:注释内容含逗号、分号等分句标点时,末尾**加**句号;纯短语**不加**句号。
```csharp
// ✔️ 含逗号 → 加句号
/// <param name="timeout">超时时间0 表示不限制。</param>
/// <returns>下载结果,包含状态码和错误信息。</returns>
// ✔️ 纯短语 → 不加句号
/// <param name="url">资源包的完整下载地址</param>
/// <returns>初始化操作句柄</returns>
```
❌ **禁止**重复方法名或参数类型中已表达的信息。
❌ **禁止**在注释中重复方法命名后缀已表达的语义。常见后缀包括:
| 命名后缀 | 已表达的语义 | 注释中禁止使用 |
|----------|------------|--------------|
| `Async` | 异步执行 | "异步" |
| `Internal` | 内部实现 | "内部" |
❌ **禁止**以"这个方法"、"该类"等冗余前缀开头。
### 4.2 常见开头约定
#### 属性 —— 描述值本身,不加动词前缀
属性的 `{ get; }` / `{ get; set; }` 访问器已表达读写语义summary 应直接描述**值的含义**,避免用"获取"、"获取或设置"等动词前缀重复已知信息。
| 属性类型 | 推荐写法 | 反例 |
|----------|---------|------|
| 只读属性 | 名词短语,如 "错误信息" | ~~"获取错误信息"~~ |
| 可读写属性 | 名词短语,如 "当前下载进度" | ~~"获取或设置当前下载进度"~~ |
| 返回 bool 的属性 | "是否……",如 "是否为只读缓存" | ~~"判断是否为只读缓存"~~ |
#### 方法 —— 以动词开头
| 成员类型 | 推荐开头动词 |
|----------|-------------|
| 返回 bool 的方法 | "检查是否"、"判断是否" |
| 获取类方法 | "获取"、"查询" |
| 设置类方法 | "设置"、"更新" |
| 创建类方法 | "创建"、"构建"、"生成" |
| 事件 | "当……时触发" |
| 构造函数 | "创建 XXX 实例"XXX 为类型的中文描述) |
### 4.3 `<param>` 书写规范
✔️ 描述参数的**含义和用途**。
✔️ 说明有效范围和边界值(如为 `null` 时的行为、取值范围)。
❌ **禁止**仅重复参数名或类型名,如"url 参数"。
### 4.4 `<returns>` 书写规范
✔️ 说明返回值的**含义**。
✔️ 说明**特殊返回值**的含义(如返回 `null` 表示未找到)。
### 4.5 `<remarks>` 书写规范
✔️ 多行内容**必须**用 `<para>` 包裹每段,否则 IDE 会将多行合并显示为一行。
```csharp
// ✔️ 好:每段用 <para> 包裹
/// <remarks>
/// <para>下载并加载 Unity AssetBundle 资源包</para>
/// <para>支持 Unity 内置缓存机制和 CRC 校验</para>
/// </remarks>
// ❌ 差多行纯文本IDE 显示时会合并为一行
/// <remarks>
/// 下载并加载 Unity AssetBundle 资源包
/// 支持 Unity 内置缓存机制和 CRC 校验
/// </remarks>
```
✔️ 单行内容可直接书写,无需 `<para>`
---
## 五、注释示例
### 5.1 类注释
```csharp
/// <summary>
/// 资源包下载请求,负责管理单个资源包的下载生命周期。
/// </summary>
/// <remarks>
/// <para>支持断点续传和失败重试</para>
/// <para>通过 <see cref="DownloadRetryController"/> 控制重试策略</para>
/// </remarks>
public class BundleDownloadRequest
{
}
```
### 5.2 方法注释
```csharp
/// <summary>
/// 从指定 URL 下载资源包并保存到本地磁盘
/// </summary>
/// <param name="url">资源包的完整下载地址</param>
/// <param name="savePath">本地保存的目标路径</param>
/// <param name="timeout">超时时间0 表示不限制。</param>
/// <returns>下载结果,包含状态码和错误信息。</returns>
public async Task<DownloadResult> DownloadAsync(string url, string savePath, int timeout = 30)
{
}
```
### 5.3 属性注释
```csharp
/// <summary>
/// 当前下载进度
/// </summary>
/// <value>取值范围 0.0 ~ 1.0,其中 1.0 表示下载完成。</value>
public float Progress { get; private set; }
```
### 5.4 枚举注释
```csharp
/// <summary>
/// 下载任务的运行状态
/// </summary>
public enum DownloadStatus
{
/// <summary>
/// 等待中,尚未开始下载。
/// </summary>
Pending,
/// <summary>
/// 正在下载中
/// </summary>
Downloading,
/// <summary>
/// 下载已完成
/// </summary>
Completed,
/// <summary>
/// 下载失败
/// </summary>
Failed
}
```
### 5.5 泛型类注释
```csharp
/// <summary>
/// 通用对象池,提供对象的复用管理。
/// </summary>
/// <typeparam name="T">池化对象的类型,必须实现 <see cref="IDisposable"/>。</typeparam>
public class ObjectPool<T> where T : IDisposable
{
}
```
### 5.6 接口注释
```csharp
/// <summary>
/// 定义下载请求的标准行为
/// </summary>
public interface IDownloadRequest
{
/// <summary>
/// 请求的远程 URL
/// </summary>
string URL { get; }
/// <summary>
/// 取消当前下载请求
/// </summary>
void Abort();
}
```
### 5.7 使用 `<inheritdoc/>`
#### 接口实现 —— 使用 `<inheritdoc/>`
实现接口成员时,使用 `<inheritdoc/>` 继承接口中定义的文档:
```csharp
public class MyDownloadRequest : IDownloadRequest
{
/// <inheritdoc/>
public string URL { get; }
/// <inheritdoc/>
public void Abort()
{
}
}
```
#### 基类继承override —— 不添加任何注释
重写基类方法时,**不需要**添加任何 XML 文档注释(包括 `<inheritdoc/>`)。方法签名中的 `override` 关键字已明确表达继承关系IDE 会自动展示基类文档。
```csharp
// ✔️ 好override 方法不添加注释
protected override void InternalStart()
{
}
protected override void InternalUpdate()
{
}
// ❌ 差:多余的 inheritdoc
/// <inheritdoc/>
protected override void InternalStart()
{
}
```
### 5.8 使用 `<example>` 和 `<code>`
```csharp
/// <summary>
/// 根据资源路径加载资源对象
/// </summary>
/// <param name="assetPath">资源路径</param>
/// <returns>资源操作句柄,加载失败时资源对象为 null。</returns>
/// <example>
/// 加载一个预制体:
/// <code>
/// var handle = package.LoadAssetAsync&lt;GameObject&gt;("Assets/Prefabs/Player.prefab");
/// await handle.Task;
/// var prefab = handle.AssetObject as GameObject;
/// </code>
/// </example>
public AssetHandle LoadAssetAsync<T>(string assetPath)
{
}
```
---
## 六、反面示例
### 6.1 冗余注释
```csharp
// ❌ 差:重复方法签名中已有的信息
/// <summary>
/// 下载方法,参数是 url 和 savePath。
/// </summary>
public void Download(string url, string savePath) { }
// ✔️ 好:说明行为和目的
/// <summary>
/// 从远程服务器下载资源包并保存到本地磁盘
/// </summary>
/// <param name="url">资源包的完整下载地址</param>
/// <param name="savePath">本地保存的目标路径</param>
public void Download(string url, string savePath) { }
```
### 6.2 废话参数注释
```csharp
// ❌ 差:仅重复参数名
/// <param name="name">名称</param>
// ✔️ 好:说明含义和约束
/// <param name="name">资源包的唯一标识名称</param>
```
### 6.3 重复命名后缀
```csharp
// ❌ 差:方法名 Async 后缀已表达异步语义
/// <summary>
/// 异步加载资源对象
/// </summary>
public AssetHandle LoadAssetAsync(string location) { }
// ✔️ 好:描述功能意图,不重复后缀语义
/// <summary>
/// 加载资源对象
/// </summary>
public AssetHandle LoadAssetAsync(string location) { }
```
### 6.4 缺少边界说明
```csharp
// ❌ 差:未说明返回 null 的情况
/// <returns>资源对象</returns>
// ✔️ 好:说明特殊返回值
/// <returns>加载到的资源对象</returns>
```
---
## 七、行内注释规范
对于非 XML 文档注释(`//` 普通注释),遵循以下原则:
### 7.1 基本准则
✔️ 解释"**为什么**",而非"是什么"——代码本身应说明"做什么",注释解释不明显的意图或约束。
✔️ 标记待办事项使用 `// TODO:` 前缀。
✔️ 标记临时方案使用 `// HACK:` 前缀。
✔️ 修改代码时**同步更新**相关注释,过时的注释比没有注释更有害。
❌ **禁止**注释显而易见的代码,如 `// 递增计数器``// 返回结果`
❌ **禁止**用注释替代清晰的命名——如果需要注释来解释变量含义,应优先改善变量名。
### 7.2 分段注释(例外)
在较长的方法体中,允许使用**简短的分段注释**充当逻辑区域标题,帮助快速定位代码段落。这类注释虽然在形式上可能"复述"了下方代码的行为,但其核心价值是**导航与分区**,不属于上述"禁止复述"的范畴。
```csharp
// ✔️ 好:在长方法中用分段注释划分逻辑区域
public void InitViewer()
{
// 加载布局文件
_visualAsset = UxmlLoader.LoadWindowUxml<DebuggerBundleListViewer>();
...
// 资源包列表
_bundleTableView = _root.Q<TableViewer>("BundleTableView");
...
// 使用列表
_usingTableView = _root.Q<TableViewer>("UsingTableView");
...
// 面板分屏
var topGroup = _root.Q<VisualElement>("TopGroup");
...
}
```
判断标准:如果移除该注释后,读者需要逐行阅读才能理解方法内的结构,则应保留。
### 7.3 示例
```csharp
// ❌ 差:复述代码
// 如果计数大于最大值,重置为零
if (count > maxCount)
count = 0;
// ✔️ 好:解释约束
// 环形缓冲区写满后从头覆盖,避免无限增长
if (count > maxCount)
count = 0;
```
---
## 八、术语翻译映射表
> 为保持注释用语统一,以下列出项目中常用英文类型/概念与其中文翻译的对应关系。
> 撰写或审核注释时,请以此表为准。
### 8.1 核心领域类型
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `PackageBundle` | 资源包描述 | 清单中的静态元数据(`Package*` = 描述) |
| `PackageAsset` | 资源描述 | 清单中的静态元数据(`Package*` = 描述) |
| `AssetInfo` | 资源信息 | 运行时上下文(`*Info` = 信息) |
| `BundleInfo` | 资源包信息 | 运行时上下文(`*Info` = 信息) |
| `ResourcePackage` | 资源包裹 | 顶层包裹容器 |
| `PackageManifest` | 包裹清单 / 资源清单 | — |
| `AssetBundle` | AssetBundle | Unity 引擎类型,保留英文 |
| `RawBundle` | 原生资源包 | 非 AssetBundle 的原始文件 |
### 8.2 文件系统与缓存
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `IFileSystem` | 文件系统 | — |
| `IBundleCache` | 缓存系统 | — |
| `ICacheEntry` | 缓存条目 | — |
| `BundleGuid` | 资源包 GUID / Bundle 唯一标识 | — |
| `ICacheEvictionPolicy` | 淘汰策略 | — |
| `EvictionResult` | 淘汰结果 | — |
### 8.3 下载与网络
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `IDownloadRequest` | 下载请求 | — |
| `IDownloadBackend` | 下载后台 | — |
| `IRemoteService` | 远端资源服务 | — |
| `IDownloadRetryPolicy` | 下载重试策略 | — |
| `IDownloadUrlPolicy` | URL 选择策略 | — |
### 8.4 加密与解密
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `IBundleEncryptor` | 资源包加密器 | — |
| `IBundleDecryptor` | 资源包解密器 | 基接口,需实现派生接口 |
| `IManifestEncryptor` | 资源清单加密器 | — |
| `IManifestDecryptor` | 资源清单解密器 | — |
| `BundleEncryptArgs` | 加密操作的输入参数 | — |
| `BundleDecryptArgs` | 解密操作的输入参数 | — |
### 8.5 资源加载与句柄
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `IBundleHandle` | 资源包句柄 | — |
| `AssetHandle` | 资源句柄 | — |
| `RawFileHandle` | 原生文件句柄 | — |
| `SubAssetsHandle` | 子资源句柄 | — |
### 8.6 通用术语
| 英文术语 | 中文翻译 | 备注 |
|---------|---------|------|
| Package | 包裹 | 项目级容器 |
| Bundle | 资源包 | 构建产物单元 |
| Asset | 资源 | 单个资源文件 |
| Manifest | 清单 | — |
| Handle | 句柄 | — |
| Operation | 操作 / 异步操作 | — |
| Scheduler | 调度器 | — |
| Provider | 提供者 | — |

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 5673a09d2666eb847a78066794039d23
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,200 +0,0 @@
# 逻辑检测
> **总则**:检查运行时代码的关键逻辑路径,确保宏分支、异步操作生命周期、参数完整性和资源释放均无遗漏。
## 1. 宏包裹代码逻辑
检查所有 `#if` / `#elif` / `#else` / `#endif` 包裹的代码:
- 每个分支的变量赋值路径是否完整(不存在未赋值分支)
- `#else` 兜底分支是否覆盖所有未列出的平台
- 各分支之间的语义是否一致(同一变量在不同分支中的类型和用途是否相同)
- 分支中引用的 API 是否与对应宏版本匹配(如 `UNITY_2020_3_OR_NEWER` 对应的 API
## 2. 子异步操作检查
**定义**:通过 `AddChildOperation()` 添加的操作为子异步操作。
### 2.1 同步等待传播
如果类实现了 `InternalWaitForCompletion()` 方法,所有子异步操作必须在该方法中主动调用 `WaitForCompletion()`
```
❌ 父操作实现了 InternalWaitForCompletion但未对子操作调用 WaitForCompletion
✅ 父操作在 InternalWaitForCompletion 中对每个子操作调用 WaitForCompletion()
```
### 2.2 更新驱动
所有子异步操作必须在父操作的 `InternalUpdate()` 中主动调用 `UpdateOperation()`,确保子操作能被正常驱动。
```
❌ 子操作被 AddChildOperation 添加后,未在 InternalUpdate 中调用 UpdateOperation
✅ 每个子操作在 InternalUpdate 中被调用 UpdateOperation()
```
## 3. 异步操作参数完整性
检查所有 Operation 的 Options 在 `new` 的位置是否完整填充了所有成员字段。
- 找到 Options 结构体的所有字段定义
- 找到所有 `new XxxOptions(...)` 的调用点
- 逐一比对构造参数是否覆盖了所有必填字段
- 可选参数(有默认值)可以跳过
```
❌ new DownloadFileRequestOptions(url, savePath, timeout) // 遗漏了 watchdogTimeout
✅ new DownloadFileRequestOptions(url, savePath, timeout, watchdogTimeout)
```
## 4. 整数溢出与取模安全
检查所有对 `int` 计数器执行 `%`(取模)运算的位置,确保不会因溢出产生负索引。
- 只增不减的 `int` 计数器(如失败计数、轮转索引),溢出 `int.MaxValue` 后变为负数
- 负数 `%` 正数在 C# 中结果为负数,用作数组/列表索引会触发 `ArgumentOutOfRangeException`
- 使用 `& 0x7FFFFFFF` 清除符号位,保证结果始终为非负整数(.NET 中处理哈希取模的惯用手法)
```
❌ int index = _counter % list.Count; // _counter 溢出后 index 为负
✅ int index = (_counter & 0x7FFFFFFF) % list.Count; // 始终非负
```
## 5. 下载请求资源释放
### 5.1 IDownloadRequest 释放
检查所有持有 `IDownloadRequest`(及其子接口)实例的 Operation是否在 `InternalDispose()` 中调用了 `Dispose()` 释放资源。
```
❌ Operation 持有 IDownloadFileRequest 但未在 InternalDispose 中释放
✅ protected override void InternalDispose() { _request?.Dispose(); }
```
### 5.2 下载数据完整性验证
检查文件数据下载完成后,是否有容错或数据完整性验证:
- 是否校验文件大小(下载字节数与预期是否一致)
- 是否校验文件哈希CRC / MD5 / SHA 等)
- 如果无校验,是否有其他容错机制(如重试、回退)
- 无任何验证且无容错机制的,标记为需要关注
## 6. 计时方式一致性
检查所有需要"测量经过时间"的逻辑,确保计时方式不会因帧率波动或 App 后台恢复而失真。
- 使用 `Time.unscaledDeltaTime` 累加计时的方式,在 App 从后台恢复时会产生巨大 spike一帧跨越几十秒导致等待逻辑在单帧内被直接跳过
- 同一系统内应保持计时方式统一,避免一处用绝对时间戳、另一处用帧增量
- 推荐使用 `TimeUtility.RealtimeSinceStartup` 记录起止时间戳做差值判断
```
❌ _elapsed += Time.unscaledDeltaTime; return _elapsed >= delay; // 后台恢复时 spike 跳过等待
✅ return TimeUtility.RealtimeSinceStartup - _startTime >= delay; // 绝对时间戳,不受 spike 影响
```
## 7. 子类回调不得覆写基类已确定的状态
检查所有基类定义的"成功/失败回调"(如 `OnRequestSucceeded``OnRequestFailed`),子类在重写时不应直接修改基类已确定的 `Status`
- 基类在调用回调前已设置 `Status = Succeeded`,若子类在回调中将其改为 `Failed`,后续基类逻辑可能与实际状态不一致
- 回调应仅负责提取结果或设置错误信息,状态转换应由基类统一控制
- 推荐将回调签名改为返回 `bool`(或通过 out 参数传递错误),基类根据返回值决定最终状态
```
❌ protected override void OnRequestSucceeded()
{
if (result == null) Status = Failed; // 子类直接覆写基类已确定的 Succeeded 状态
}
✅ protected override bool OnRequestSucceeded()
{
if (result == null) { Error = "..."; return false; } // 返回 false由基类设置 Failed
return true;
}
```
## 8. 结构体不可变性检测
> 依据:[类型设计准则](2.类型设计准则.md) — "❌ **禁止**定义可变值类型" / "✔️ 声明不可变值类型时使用 `readonly struct` 修饰符"
检查所有 `struct` 定义,**必须**声明为 `readonly struct`。存在可变 struct 即为违规。
检测要点:
- `struct` 声明缺少 `readonly` 修饰符
- 属性使用 `{ get; set; }` 而非 `{ get; }`
- 公共字段未声明为 `readonly`
- 缺少构造函数(依赖默认值逐字段赋值的模式)
```
❌ struct MutableConfig
{
public int Timeout { get; set; } // 可变属性
public string Name { get; set; }
}
✅ readonly struct ImmutableConfig
{
public int Timeout { get; } // 只读属性
public string Name { get; }
public ImmutableConfig(int timeout, string name)
{
Timeout = timeout;
Name = name;
}
}
```
**禁止可变 struct 的原因**
- 值类型按值复制,可变成员修改作用于副本而非原值,极易产生静默 bug
- `readonly` 字段持有可变 struct 时,编译器每次访问都会产生防御性复制,造成不必要的性能开销
- `readonly struct` 从根源上杜绝以上两类问题
## 9. 文件系统禁止重写 GetHashCode
检查所有 `IFileSystem` 实现类,确认未重写 `GetHashCode()``Equals()`
`BundleInfo.GetCombineKey()` 依赖 `RuntimeHelpers.GetHashCode(_fileSystem)` 获取基于引用标识的哈希值,用于下载器合并时的去重判断。如果文件系统实现类重写了 `GetHashCode()`,虽然 `RuntimeHelpers.GetHashCode` 本身不受影响,但会暗示该类型有自定义相等性语义,可能导致其他使用 `GetHashCode()` 的位置产生非预期的碰撞。
检测要点:
- `IFileSystem` 实现类中存在 `override int GetHashCode()` 声明
- `IFileSystem` 实现类中存在 `override bool Equals(object)` 声明
```
❌ class SandboxFileSystem : IFileSystem
{
public override int GetHashCode() => PackageName.GetHashCode(); // 破坏引用标识语义
}
✅ class SandboxFileSystem : IFileSystem
{
// 不重写 GetHashCode保持 object 默认的引用标识行为
}
```
## 10. 不可变结构体命名参数检测
检测所有 `readonly struct` 构造函数调用中**位置参数 > 3 个**的位置,确认是否使用命名参数提高可读性。
`readonly struct` 只能通过构造函数一次性传入所有字段,参数数量通常较多,且参数顺序是唯一区分手段。同类型参数相邻时,顺序错误不会产生编译错误,容易引入静默 bug。
重点检查以下场景:
- 构造函数参数 > 3 个
- 多个同类型参数相邻(如连续多个 `bool``int`、接口类型)
- 参数名与属性名高度对应,使用命名参数可消除歧义
```
❌ new Configuration(60, true, level, decryptor, backend, retryPolicy, urlPolicy);
✅ new Configuration(
watchdogTimeout: 60,
disableUnityWebCache: true,
downloadVerifyLevel: level,
assetBundleDecryptor: decryptor,
downloadBackend: backend,
downloadRetryPolicy: retryPolicy,
downloadUrlPolicy: urlPolicy);
```

View File

@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 200087470392cf24ebdd0efac8e44210
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,5 +1,5 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
// 外部友元 // 外部友元
[assembly: InternalsVisibleTo("YooAsset.EditorExtension")] [assembly: InternalsVisibleTo("YooAsset.Extension.Editor")]
[assembly: InternalsVisibleTo("Assembly-CSharp-Editor")] [assembly: InternalsVisibleTo("Assembly-CSharp-Editor")]

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: b819eb7cc257867439445ce1a660ea0a guid: da26c791ca572fe4a818405173661568
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@@ -0,0 +1,112 @@
using System;
using System.IO;
using System.Collections.Generic;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建辅助工具类
/// </summary>
internal static class ArchiveFileBuildHelper
{
private const int StreamCopyBufferSize = 81920;
/// <summary>
/// 收集构建资源的归档条目列表,并按 FilePath 字典序排序
/// </summary>
public static List<ArchiveFileEntry> CollectEntries(IReadOnlyList<BuildAssetInfo> allAssets)
{
var entries = new List<ArchiveFileEntry>(allAssets.Count);
foreach (var asset in allAssets)
{
string assetPath = asset.AssetInfo.AssetPath;
long dataLength = new FileInfo(assetPath).Length;
uint crc = HashUtility.ComputeFileCrc32AsUInt(assetPath);
entries.Add(new ArchiveFileEntry(assetPath, dataLength, crc));
}
entries.Sort((a, b) => string.Compare(a.AssetPath, b.AssetPath, StringComparison.Ordinal));
return entries;
}
/// <summary>
/// 计算每个条目的绝对偏移,并写入归档文件
/// </summary>
/// <param name="outputPath">输出文件路径</param>
/// <param name="entries">归档条目列表</param>
/// <param name="fileAlignment">文件数据对齐字节数0 表示不对齐)</param>
public static void BuildArchiveFile(string outputPath, List<ArchiveFileEntry> entries, int fileAlignment)
{
int fileCount = entries.Count;
if (fileCount > ArchiveBundleConsts.MaxChildFileCount)
throw new InvalidOperationException($"Archive child file count {fileCount} exceeds maximum ({ArchiveBundleConsts.MaxChildFileCount}).");
// 1. 计算 header 总大小
int headerSize = 4 + 4 + 4; // Magic + Version + FileCount
foreach (var entry in entries)
{
byte[] pathBytes = entry.GetPathBytes();
// FilePathLen(4) + FilePath(变长) + DataOffset(8) + DataLength(8) + FileCRC(4)
headerSize += 4 + pathBytes.Length + 8 + 8 + 4;
}
// 2. 计算每个文件的绝对偏移
long currentOffset = headerSize;
foreach (var entry in entries)
{
if (fileAlignment > 0)
currentOffset = AlignOffset(currentOffset, fileAlignment);
entry.DataOffset = currentOffset;
currentOffset += entry.DataLength;
}
// 3. 写入归档文件
EditorFileUtility.CreateFileDirectory(outputPath);
using (var fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
using (var writer = new BinaryWriter(fs))
{
// Header
writer.Write(ArchiveBundleConsts.FileMagic);
writer.Write(ArchiveBundleConsts.FileVersion);
writer.Write(fileCount);
foreach (var entry in entries)
{
byte[] pathBytes = entry.GetPathBytes();
writer.Write(pathBytes.Length);
writer.Write(pathBytes);
writer.Write(entry.DataOffset);
writer.Write(entry.DataLength);
writer.Write(entry.FileCRC);
}
// Data: 按排序后的顺序写入,使用流式拷贝避免大文件 OOM
byte[] buffer = new byte[StreamCopyBufferSize];
foreach (var entry in entries)
{
// 填充对齐字节
long paddingSize = entry.DataOffset - fs.Position;
if (paddingSize > 0)
writer.Write(new byte[paddingSize]);
using (var sourceStream = new FileStream(entry.AssetPath, FileMode.Open, FileAccess.Read))
{
int bytesRead;
while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
{
writer.Write(buffer, 0, bytesRead);
}
}
}
}
}
/// <summary>
/// 将偏移值向上对齐到指定字节边界
/// </summary>
private static long AlignOffset(long offset, int alignment)
{
return (offset + alignment - 1) / alignment * alignment;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: ec9d17e765ccd4642aa3aa9ca0580799 guid: 3f8daa3aacb5da8458fc073431da1891
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@@ -0,0 +1,46 @@
using System;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的构建参数
/// </summary>
public class ArchiveFileBuildParameters : BuildParameters
{
private const int MaxFileAlignment = 4096;
/// <summary>
/// 文件哈希值计算包含路径信息
/// </summary>
public bool IncludePathInHash { get; set; } = false;
/// <summary>
/// 归档文件内数据对齐字节数0 表示不对齐)
/// </summary>
/// <remarks>
/// 对齐后每个子文件的数据偏移会向上取整到该值的整数倍,文件间以零字节填充。
/// 推荐值4通用对齐、512磁盘扇区对齐、4096内存页对齐
/// </remarks>
public int FileAlignment { get; set; } = 0;
/// <inheritdoc />
protected override void CheckBuildParametersCore()
{
// ArchiveBundle 不支持资源包加密
if (BundleEncryptor != null)
{
string message = BuildLogger.GetErrorMessage(ErrorCode.BundleEncryptionNotSupported,
$"ArchiveFileBuildPipeline does not support bundle encryption. Please remove the BundleEncryptor configuration.");
throw new NotSupportedException(message);
}
// 校验文件对齐参数范围
if (FileAlignment < 0 || FileAlignment > MaxFileAlignment)
{
throw new ArgumentOutOfRangeException(nameof(FileAlignment),
$"FileAlignment must be between 0 and {MaxFileAlignment}. Current value: {FileAlignment}.");
}
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: b974d3d744622f3499d026f99074cd72 guid: 83bdf0f6502d23f49ae900d98f459c7b
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线,将同一 BundleName 下的多个原始文件合并写入一个归档文件
/// </summary>
public class ArchiveFileBuildPipeline : IBuildPipeline
{
/// <summary>
/// 执行构建流程
/// </summary>
public BuildResult Run(BuildParameters buildParameters, bool enableLog)
{
if (buildParameters is ArchiveFileBuildParameters)
{
BundleBuilder builder = new BundleBuilder();
return builder.Run(buildParameters, GetDefaultBuildPipeline(), enableLog);
}
else
{
throw new ArgumentException($"Invalid build parameter type: '{buildParameters.GetType().Name}'.", nameof(buildParameters));
}
}
/// <summary>
/// 获取默认的构建流程
/// </summary>
private List<IBuildTask> GetDefaultBuildPipeline()
{
List<IBuildTask> pipeline = new List<IBuildTask>
{
new TaskPrepare_AFBP(),
new TaskGetBuildMap_AFBP(),
new TaskBuilding_AFBP(),
new TaskEncryption_AFBP(),
new TaskUpdateBundleInfo_AFBP(),
new TaskCreateManifest_AFBP(),
new TaskCreateReport_AFBP(),
new TaskCreatePackage_AFBP(),
new TaskCopyBundledFiles_AFBP(),
new TaskCreateCatalog_AFBP()
};
return pipeline;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 41ab464f315df234fb40b0c24e97ee23 guid: 19ef6fc50bd422c4db6fa53cbf0fe226
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@@ -0,0 +1,61 @@
using System;
using System.Text;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件条目信息(构建期临时数据结构)
/// </summary>
internal class ArchiveFileEntry
{
private byte[] _pathBytes;
/// <summary>
/// 文件路径
/// </summary>
public readonly string AssetPath;
/// <summary>
/// 文件数据长度
/// </summary>
public readonly long DataLength;
/// <summary>
/// 文件 CRC32
/// </summary>
public readonly uint FileCRC;
/// <summary>
/// 数据在归档文件中的绝对偏移
/// </summary>
public long DataOffset;
/// <summary>
/// 构造归档文件条目
/// </summary>
public ArchiveFileEntry(string assetPath, long dataLength, uint fileCRC)
{
if (string.IsNullOrEmpty(assetPath))
throw new ArgumentException("Asset path is null or empty.", nameof(assetPath));
if (dataLength < 0)
throw new ArgumentException($"Invalid data length {dataLength} for '{assetPath}'.", nameof(dataLength));
if (dataLength > ArchiveBundleConsts.MaxChildFileSize)
throw new ArgumentException($"Child file exceeds maximum size ({ArchiveBundleConsts.MaxChildFileSize} bytes): '{assetPath}' ({dataLength} bytes).", nameof(dataLength));
AssetPath = assetPath;
DataLength = dataLength;
FileCRC = fileCRC;
}
/// <summary>
/// 获取文件路径的 UTF8 字节缓存
/// </summary>
public byte[] GetPathBytes()
{
if (_pathBytes == null)
_pathBytes = Encoding.UTF8.GetBytes(AssetPath);
return _pathBytes;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 19ed2b81fece5b84fa771a5eeb108572 guid: a03062dc3dd175c438fcb92d3e06a33b
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 2e49e0ae672b9944783571b2ad737df9 guid: b9dccc7a4589001469444e571235890a
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@@ -0,0 +1,29 @@
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的核心构建任务
/// </summary>
public class TaskBuilding_AFBP : IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
var buildMapContext = context.GetContextObject<BuildMapContext>();
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildParameters = buildParametersContext.Parameters as ArchiveFileBuildParameters;
string pipelineOutputDirectory = buildParametersContext.GetPipelineOutputDirectory();
int progressValue = 0;
int fileTotalCount = buildMapContext.Collection.Count;
foreach (var bundleInfo in buildMapContext.Collection)
{
string archiveFilePath = $"{pipelineOutputDirectory}/{bundleInfo.BundleName}";
var entries = ArchiveFileBuildHelper.CollectEntries(bundleInfo.AllPackAssets);
ArchiveFileBuildHelper.BuildArchiveFile(archiveFilePath, entries, buildParameters.FileAlignment);
EditorDialogUtility.DisplayProgressBar("Build archive files", ++progressValue, fileTotalCount);
}
EditorDialogUtility.ClearProgressBar();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 289e760820002e34b9054a81098d5fa4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using System;
using UnityEditor;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的首包资源拷贝任务
/// </summary>
public class TaskCopyBundledFiles_AFBP : TaskCopyBundledFiles, IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildParameters = buildParametersContext.Parameters;
var manifestContext = context.GetContextObject<ManifestContext>();
if (buildParameters.BundledCopyOption != EBundledCopyOption.None)
{
CopyBundledFilesToStreaming(buildParametersContext, manifestContext.Manifest);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e4fb32a77b3e799448cf4eae2a97a49e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
using System;
using UnityEditor;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的资源目录创建任务
/// </summary>
public class TaskCreateCatalog_AFBP : TaskCreateCatalog, IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
if (buildParametersContext.Parameters.BundledCopyOption != EBundledCopyOption.None)
{
CreateCatalogFile(buildParametersContext);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 813f7821d87b6f8498ddc4d675512413
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
using System;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的清单创建任务
/// </summary>
public class TaskCreateManifest_AFBP : TaskCreateManifest, IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
CreateManifestFile(false, true, false, context);
}
protected override string[] GetBundleDepends(BuildContext context, string bundleName)
{
return Array.Empty<string>();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 21e10b308293d2e45972a9b4ba143a74
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的补丁包创建任务
/// </summary>
public class TaskCreatePackage_AFBP : TaskCreatePackage, IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildMapContext = context.GetContextObject<BuildMapContext>();
CreatePackagePatch(buildParametersContext, buildMapContext);
}
private void CreatePackagePatch(BuildParametersContext buildParametersContext, BuildMapContext buildMapContext)
{
string packageOutputDirectory = buildParametersContext.GetPackageOutputDirectory();
BuildLogger.Log($"Start making patch package: '{packageOutputDirectory}'.");
CopyPackageBundles(buildMapContext);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 721f0030551d41c40a2c6d3206c3897a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的构建报告创建任务
/// </summary>
public class TaskCreateReport_AFBP : TaskCreateReport, IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
var buildParameters = context.GetContextObject<BuildParametersContext>();
var buildMapContext = context.GetContextObject<BuildMapContext>();
var manifestContext = context.GetContextObject<ManifestContext>();
CreateReportFile(buildParameters, buildMapContext, manifestContext);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3e12187230b544940aeae7cb3c556299
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的加密任务
/// </summary>
public class TaskEncryption_AFBP : TaskEncryption, IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
var buildParameters = context.GetContextObject<BuildParametersContext>();
var buildMapContext = context.GetContextObject<BuildMapContext>();
EncryptingBundleFiles(buildParameters, buildMapContext);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 16ff5758e3a98b64193c61ca68e94ddd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的构建映射生成任务
/// </summary>
public class TaskGetBuildMap_AFBP : TaskGetBuildMap, IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildMapContext = CreateBuildMap(true, buildParametersContext.Parameters);
context.SetContextObject(buildMapContext);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 409de066b34f4b64fa9eaee790045492
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,31 @@
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的准备任务
/// </summary>
public class TaskPrepare_AFBP : TaskPrepare, IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildParameters = buildParametersContext.Parameters;
// 检测构建参数
buildParametersContext.CheckBuildParameters();
// 检测未保存场景
CheckDirtyScenes();
// 删除历史缓存
if (buildParameters.ClearBuildCacheFiles)
{
DeletePackageRootDirectory(buildParameters);
}
// 准备输出目录
PrepareOutputDirectory(buildParameters);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f463d370e0512bb4c8d79e22fe149500
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,51 @@
using System;
using System.IO;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线的资源包信息更新任务
/// </summary>
public class TaskUpdateBundleInfo_AFBP : TaskUpdateBundleInfo, IBuildTask
{
/// <inheritdoc/>
void IBuildTask.Run(BuildContext context)
{
UpdateBundleInfo(context);
}
protected override string GetUnityHash(BuildBundleInfo bundleInfo, BuildContext context)
{
return ComputeFileHash(bundleInfo, context);
}
protected override uint GetUnityCRC(BuildBundleInfo bundleInfo, BuildContext context)
{
return 0;
}
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildContext context)
{
return ComputeFileHash(bundleInfo, context);
}
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.ComputeFileCrc32AsUInt(filePath);
}
protected override long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return FileUtility.GetFileSize(filePath);
}
private string ComputeFileHash(BuildBundleInfo bundleInfo, BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var parameters = buildParametersContext.Parameters as ArchiveFileBuildParameters;
string filePath = bundleInfo.PackageSourceFilePath;
if (parameters.IncludePathInHash)
return GetFileMD5IncludePath(filePath);
else
return HashUtility.ComputeFileMD5(filePath);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6263649b351967840836e9ca443c5574
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,5 +1,4 @@
 namespace YooAsset.Editor
namespace YooAsset.Editor
{ {
/// <summary> /// <summary>
/// 编辑器模拟构建管线的构建映射生成任务 /// 编辑器模拟构建管线的构建映射生成任务
@@ -13,10 +12,17 @@ namespace YooAsset.Editor
var buildMapContext = CreateBuildMap(true, buildParametersContext.Parameters); var buildMapContext = CreateBuildMap(true, buildParametersContext.Parameters);
context.SetContextObject(buildMapContext); context.SetContextObject(buildMapContext);
if (buildParametersContext.Parameters.BuildBundleType == (int)EBundleType.RawBundle) // 注意:检查每个原生文件资源包只能包含一个原生文件
if (buildParametersContext.Parameters.BuildBundleType == (int)EBundleType.VirtualRawBundle)
{ {
CheckRawBundleMapContent(buildMapContext); CheckRawBundleMapContent(buildMapContext);
} }
// 检查归档资源包内每个子文件大小不超过上限
if (buildParametersContext.Parameters.BuildBundleType == (int)EBundleType.VirtualArchiveBundle)
{
CheckArchiveBundleMapContent(buildMapContext);
}
} }
} }
} }

View File

@@ -1,4 +1,3 @@
using System;
namespace YooAsset.Editor namespace YooAsset.Editor
{ {

View File

@@ -1,3 +1,4 @@
using System;
namespace YooAsset.Editor namespace YooAsset.Editor
{ {
@@ -6,5 +7,18 @@ namespace YooAsset.Editor
/// </summary> /// </summary>
public class EditorSimulateBuildParameters : BuildParameters public class EditorSimulateBuildParameters : BuildParameters
{ {
/// <inheritdoc />
protected override void CheckBuildParametersCore()
{
// EditorSimulateBuildPipeline 只允许 VirtualBundle 类型
if (BuildBundleType != (int)EBundleType.VirtualAssetBundle &&
BuildBundleType != (int)EBundleType.VirtualRawBundle &&
BuildBundleType != (int)EBundleType.VirtualArchiveBundle)
{
string message = BuildLogger.GetErrorMessage(ErrorCode.BuildBundleTypeNotSupported,
$"{nameof(EditorSimulateBuildPipeline)} only supports VirtualBundle types. Received: {(EBundleType)BuildBundleType}.");
throw new InvalidOperationException(message);
}
}
} }
} }

View File

@@ -1,4 +1,3 @@
using System;
namespace YooAsset.Editor namespace YooAsset.Editor
{ {

View File

@@ -36,7 +36,6 @@ namespace YooAsset.Editor
/// <remarks>开启此项可以节省运行时清单占用的内存</remarks> /// <remarks>开启此项可以节省运行时清单占用的内存</remarks>
public bool ReplaceAssetPathWithAddress = false; public bool ReplaceAssetPathWithAddress = false;
/// <summary> /// <summary>
/// 获取旧版构建管线的构建选项 /// 获取旧版构建管线的构建选项
/// </summary> /// </summary>

View File

@@ -1,8 +1,5 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor; using UnityEditor;
using UnityEngine;
namespace YooAsset.Editor namespace YooAsset.Editor
{ {

View File

@@ -20,8 +20,6 @@ namespace YooAsset.Editor
{ {
string packageOutputDirectory = buildParametersContext.GetPackageOutputDirectory(); string packageOutputDirectory = buildParametersContext.GetPackageOutputDirectory();
BuildLogger.Log($"Start making patch package: '{packageOutputDirectory}'."); BuildLogger.Log($"Start making patch package: '{packageOutputDirectory}'.");
// 拷贝所有补丁文件
CopyPackageBundles(buildMapContext); CopyPackageBundles(buildMapContext);
} }
} }

View File

@@ -1,4 +1,3 @@
using System;
namespace YooAsset.Editor namespace YooAsset.Editor
{ {
@@ -16,6 +15,9 @@ namespace YooAsset.Editor
// 检测构建参数 // 检测构建参数
buildParametersContext.CheckBuildParameters(); buildParametersContext.CheckBuildParameters();
// 检测未保存场景
CheckDirtyScenes();
// 删除历史缓存 // 删除历史缓存
if (buildParameters.ClearBuildCacheFiles) if (buildParameters.ClearBuildCacheFiles)
{ {

View File

@@ -1,8 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
namespace YooAsset.Editor namespace YooAsset.Editor
{ {
@@ -19,18 +16,7 @@ namespace YooAsset.Editor
protected override string GetUnityHash(BuildBundleInfo bundleInfo, BuildContext context) protected override string GetUnityHash(BuildBundleInfo bundleInfo, BuildContext context)
{ {
var buildParametersContext = context.GetContextObject<BuildParametersContext>(); return ComputeFileHash(bundleInfo, context);
var rawFileBuildParameters = buildParametersContext.Parameters as RawFileBuildParameters;
if (rawFileBuildParameters.IncludePathInHash)
{
string filePath = bundleInfo.PackageSourceFilePath;
return GetFileMD5IncludePath(filePath);
}
else
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.ComputeFileMD5(filePath);
}
} }
protected override uint GetUnityCRC(BuildBundleInfo bundleInfo, BuildContext context) protected override uint GetUnityCRC(BuildBundleInfo bundleInfo, BuildContext context)
{ {
@@ -38,18 +24,7 @@ namespace YooAsset.Editor
} }
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildContext context) protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildContext context)
{ {
var buildParametersContext = context.GetContextObject<BuildParametersContext>(); return ComputeFileHash(bundleInfo, context);
var rawFileBuildParameters = buildParametersContext.Parameters as RawFileBuildParameters;
if (rawFileBuildParameters.IncludePathInHash)
{
string filePath = bundleInfo.PackageSourceFilePath;
return GetFileMD5IncludePath(filePath);
}
else
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.ComputeFileMD5(filePath);
}
} }
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildContext context) protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildContext context)
{ {
@@ -62,12 +37,15 @@ namespace YooAsset.Editor
return FileUtility.GetFileSize(filePath); return FileUtility.GetFileSize(filePath);
} }
private string GetFileMD5IncludePath(string filePath) private string ComputeFileHash(BuildBundleInfo bundleInfo, BuildContext context)
{ {
string pathHash = HashUtility.ComputeMD5(filePath.ToLowerInvariant()); var buildParametersContext = context.GetContextObject<BuildParametersContext>();
string contentHash = HashUtility.ComputeFileMD5(filePath); var parameters = buildParametersContext.Parameters as RawFileBuildParameters;
string combined = pathHash + contentHash; string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.ComputeMD5(combined); if (parameters.IncludePathInHash)
return GetFileMD5IncludePath(filePath);
else
return HashUtility.ComputeFileMD5(filePath);
} }
} }
} }

View File

@@ -1,6 +1,3 @@
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
namespace YooAsset.Editor namespace YooAsset.Editor
{ {

View File

@@ -1,4 +1,3 @@
using System;
namespace YooAsset.Editor namespace YooAsset.Editor
{ {
@@ -11,7 +10,7 @@ namespace YooAsset.Editor
void IBuildTask.Run(BuildContext context) void IBuildTask.Run(BuildContext context)
{ {
var buildParametersContext = context.GetContextObject<BuildParametersContext>(); var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildParameters = buildParametersContext.Parameters as ScriptableBuildParameters; var buildParameters = buildParametersContext.Parameters;
// 检测构建参数 // 检测构建参数
buildParametersContext.CheckBuildParameters(); buildParametersContext.CheckBuildParameters();
@@ -29,13 +28,6 @@ namespace YooAsset.Editor
// 准备输出目录 // 准备输出目录
PrepareOutputDirectory(buildParameters); PrepareOutputDirectory(buildParameters);
// 检测内置着色器资源包名称
if (string.IsNullOrEmpty(buildParameters.BuiltinShadersBundleName))
{
string warning = BuildLogger.GetErrorMessage(ErrorCode.BuiltinShadersBundleNameIsNull, "Builtin shaders bundle name is null. It will cause resource redundancy.");
BuildLogger.Warning(warning);
}
} }
} }
} }

View File

@@ -72,6 +72,17 @@ namespace YooAsset.Editor
public string MonoScriptsBundleName; public string MonoScriptsBundleName;
/// <inheritdoc />
protected override void CheckBuildParametersCore()
{
// 检测内置着色器资源包名称
if (string.IsNullOrEmpty(BuiltinShadersBundleName))
{
string warning = BuildLogger.GetErrorMessage(ErrorCode.BuiltinShadersBundleNameIsNull, "Builtin shaders bundle name is null. It will cause resource redundancy.");
BuildLogger.Warning(warning);
}
}
/// <summary> /// <summary>
/// 获取可编程构建管线的构建参数 /// 获取可编程构建管线的构建参数
/// </summary> /// </summary>

View File

@@ -58,6 +58,16 @@ namespace YooAsset.Editor
/// </summary> /// </summary>
BuildBundleTypeIsUnknown = 117, BuildBundleTypeIsUnknown = 117,
/// <summary>
/// 构建管线不支持指定的资源包类型
/// </summary>
BuildBundleTypeNotSupported = 118,
/// <summary>
/// 构建管线不支持资源包加密
/// </summary>
BundleEncryptionNotSupported = 119,
/// <summary> /// <summary>
/// 建议使用 SBP 构建管线 /// 建议使用 SBP 构建管线
/// </summary> /// </summary>

View File

@@ -36,7 +36,8 @@ namespace YooAsset.Editor
// 清空首包资源目录 // 清空首包资源目录
if (copyOption == EBundledCopyOption.ClearAndCopyAll || copyOption == EBundledCopyOption.ClearAndCopyByTags) if (copyOption == EBundledCopyOption.ClearAndCopyAll || copyOption == EBundledCopyOption.ClearAndCopyByTags)
{ {
EditorFileUtility.ClearFolder(bundledRootDirectory); EditorFileUtility.DeleteDirectory(bundledRootDirectory);
EditorFileUtility.CreateDirectory(bundledRootDirectory);
} }
// 拷贝补丁清单文件 // 拷贝补丁清单文件

View File

@@ -270,5 +270,27 @@ namespace YooAsset.Editor
} }
} }
} }
/// <summary>
/// 检测归档资源包内每个子文件是否超过最大允许大小
/// </summary>
/// <param name="buildMapContext">构建映射上下文</param>
protected void CheckArchiveBundleMapContent(BuildMapContext buildMapContext)
{
foreach (var bundleInfo in buildMapContext.Collection)
{
foreach (var asset in bundleInfo.AllPackAssets)
{
string assetPath = asset.AssetInfo.AssetPath;
long fileSize = EditorFileUtility.GetFileSize(assetPath);
if (fileSize > ArchiveBundleConsts.MaxChildFileSize)
{
throw new InvalidOperationException(
$"Archive child file exceeds maximum size ({ArchiveBundleConsts.MaxChildFileSize} bytes): " +
$"'{assetPath}' ({fileSize} bytes) in bundle '{bundleInfo.BundleName}'.");
}
}
}
}
} }
} }

View File

@@ -106,5 +106,16 @@ namespace YooAsset.Editor
/// <param name="context">构建上下文</param> /// <param name="context">构建上下文</param>
/// <returns>文件大小</returns> /// <returns>文件大小</returns>
protected abstract long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildContext context); protected abstract long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildContext context);
/// <summary>
/// 计算包含路径信息的文件 MD5
/// </summary>
protected string GetFileMD5IncludePath(string filePath)
{
string pathHash = HashUtility.ComputeMD5(filePath.ToLowerInvariant());
string contentHash = HashUtility.ComputeFileMD5(filePath);
string combined = pathHash + contentHash;
return HashUtility.ComputeMD5(combined);
}
} }
} }

View File

@@ -16,9 +16,7 @@ namespace YooAsset.Editor
/// <returns>包裹构建结果</returns> /// <returns>包裹构建结果</returns>
public static PackageBuildResult SimulateBuild(PackageBuildParameters buildParam) public static PackageBuildResult SimulateBuild(PackageBuildParameters buildParam)
{ {
string packageName = buildParam.PackageName;
string buildPipelineName = buildParam.BuildPipelineName; string buildPipelineName = buildParam.BuildPipelineName;
if (buildPipelineName == EBuildPipeline.EditorSimulateBuildPipeline.ToString()) if (buildPipelineName == EBuildPipeline.EditorSimulateBuildPipeline.ToString())
{ {
var buildParameters = new EditorSimulateBuildParameters(); var buildParameters = new EditorSimulateBuildParameters();
@@ -27,7 +25,7 @@ namespace YooAsset.Editor
buildParameters.BuildPipeline = EBuildPipeline.EditorSimulateBuildPipeline.ToString(); buildParameters.BuildPipeline = EBuildPipeline.EditorSimulateBuildPipeline.ToString();
buildParameters.BuildBundleType = buildParam.BuildBundleType; buildParameters.BuildBundleType = buildParam.BuildBundleType;
buildParameters.BuildTarget = EditorUserBuildSettings.activeBuildTarget; buildParameters.BuildTarget = EditorUserBuildSettings.activeBuildTarget;
buildParameters.PackageName = packageName; buildParameters.PackageName = buildParam.PackageName;
buildParameters.PackageVersion = "Simulate"; buildParameters.PackageVersion = "Simulate";
buildParameters.FileNameStyle = EFileNameStyle.HashName; buildParameters.FileNameStyle = EFileNameStyle.HashName;
buildParameters.BundledCopyOption = EBundledCopyOption.None; buildParameters.BundledCopyOption = EBundledCopyOption.None;

View File

@@ -30,5 +30,10 @@ namespace YooAsset.Editor
/// 团结引擎 InstantAsset 构建管线 (IABP) /// 团结引擎 InstantAsset 构建管线 (IABP)
/// </summary> /// </summary>
InstantAssetBuildPipeline, InstantAssetBuildPipeline,
/// <summary>
/// 归档文件构建管线 (AFBP)
/// </summary>
ArchiveFileBuildPipeline,
} }
} }

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: cda191ef75dc59f408545d8f7d3644b0 guid: 17a0d1dece605b74fb10ceb9761147e9
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@@ -0,0 +1,166 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace YooAsset.Editor
{
/// <summary>
/// 归档文件构建管线ArchiveFileBuildPipeline的构建参数查看器
/// </summary>
[BuildPipelineAttribute(nameof(EBuildPipeline.ArchiveFileBuildPipeline))]
internal class ArchiveFileBuildPipelineViewer : BuildPipelineViewerBase
{
/// <summary>
/// 根布局容器UXML 克隆实例)
/// </summary>
protected TemplateContainer Root;
/// <summary>
/// 构建输出目录文本框
/// </summary>
protected TextField _buildOutputField;
/// <summary>
/// 构建版本文本框
/// </summary>
protected TextField _buildVersionField;
/// <summary>
/// 资源清单加密器下拉框
/// </summary>
protected PopupField<Type> _manifestEncryptorField;
/// <summary>
/// 资源清单解密器下拉框
/// </summary>
protected PopupField<Type> _manifestDecryptorField;
/// <summary>
/// 输出文件名称样式枚举字段
/// </summary>
protected EnumField _outputNameStyleField;
/// <summary>
/// 首包资源拷贝选项枚举字段
/// </summary>
protected EnumField _bundledCopyOptionField;
/// <summary>
/// 首包资源拷贝标签参数文本框
/// </summary>
protected TextField _bundledCopyParamField;
/// <summary>
/// 是否清理构建缓存开关
/// </summary>
protected Toggle _clearBuildCacheToggle;
/// <summary>
/// 是否使用资源依赖数据库开关
/// </summary>
protected Toggle _useAssetDependencyDBToggle;
public override void CreateView(VisualElement parent)
{
// 加载布局文件
var visualAsset = UxmlLoader.LoadWindowUxml<ArchiveFileBuildPipelineViewer>();
if (visualAsset == null)
return;
Root = visualAsset.CloneTree();
Root.style.flexGrow = 1f;
parent.Add(Root);
// 输出目录
_buildOutputField = Root.Q<TextField>("BuildOutput");
SetBuildOutputField(_buildOutputField);
// 构建版本
_buildVersionField = Root.Q<TextField>("BuildVersion");
SetBuildVersionField(_buildVersionField);
// 清单服务
var popupContainer = Root.Q("PopupContainer");
_manifestEncryptorField = CreateManifestEncryptorField(popupContainer);
_manifestDecryptorField = CreateManifestDecryptorField(popupContainer);
// 输出文件名称样式
_outputNameStyleField = Root.Q<EnumField>("FileNameStyle");
SetOutputNameStyleField(_outputNameStyleField);
// 首包资源拷贝参数
_bundledCopyParamField = Root.Q<TextField>("BundledCopyParam");
SetBundledCopyParamField(_bundledCopyParamField);
SetBundledCopyParamVisible(_bundledCopyParamField);
// 首包资源拷贝选项
_bundledCopyOptionField = Root.Q<EnumField>("BundledCopyOption");
SetBundledCopyOptionField(_bundledCopyOptionField, _bundledCopyParamField);
// 清理构建缓存
_clearBuildCacheToggle = Root.Q<Toggle>("ClearBuildCache");
SetClearBuildCacheToggle(_clearBuildCacheToggle);
// 使用资源依赖数据库
_useAssetDependencyDBToggle = Root.Q<Toggle>("UseAssetDependency");
SetUseAssetDependencyDBToggle(_useAssetDependencyDBToggle);
// 构建按钮
var buildButton = Root.Q<Button>("Build");
buildButton.clicked += BuildButton_clicked;
}
private void BuildButton_clicked()
{
if (EditorUtility.DisplayDialog("Info", $"Start building resource package '{PackageName}'.", "Yes", "No"))
{
EditorWindowUtility.ClearUnityConsole();
EditorApplication.delayCall += ExecuteBuild;
}
else
{
Debug.LogWarning("Packaging has been canceled.");
}
}
/// <summary>
/// 执行构建
/// </summary>
protected virtual void ExecuteBuild()
{
var fileNameStyle = BundleBuilderSetting.GetPackageFileNameStyle(PackageName, PipelineName);
var bundledCopyOption = BundleBuilderSetting.GetPackageBundledCopyOption(PackageName, PipelineName);
var bundledCopyParams = BundleBuilderSetting.GetPackageBundledCopyParams(PackageName, PipelineName);
var clearBuildCache = BundleBuilderSetting.GetPackageClearBuildCache(PackageName, PipelineName);
var useAssetDependencyDB = BundleBuilderSetting.GetPackageUseAssetDependencyDB(PackageName, PipelineName);
ArchiveFileBuildParameters buildParameters = new ArchiveFileBuildParameters();
buildParameters.BuildOutputRoot = BundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BundledFileRoot = BundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBundleType.ArchiveBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;
buildParameters.VerifyBuildingResult = true;
buildParameters.FileNameStyle = fileNameStyle;
buildParameters.BundledCopyOption = bundledCopyOption;
buildParameters.BundledCopyParams = bundledCopyParams;
buildParameters.ClearBuildCacheFiles = clearBuildCache;
buildParameters.UseAssetDependencyDB = useAssetDependencyDB;
buildParameters.ManifestEncryptor = CreateManifestEncryptorInstance();
buildParameters.ManifestDecryptor = CreateManifestDecryptorInstance();
ArchiveFileBuildPipeline pipeline = new ArchiveFileBuildPipeline();
var buildResult = pipeline.Run(buildParameters, true);
if (buildResult.Success)
EditorUtility.RevealInFinder(buildResult.OutputPackageDirectory);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 31e95ed362aa6fb43ad104edf5599b89
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<ui:VisualElement name="BuildContainer">
<ui:TextField picking-mode="Ignore" label="Build Output" name="BuildOutput" />
<ui:TextField picking-mode="Ignore" label="Build Version" name="BuildVersion" />
<ui:Toggle label="Clear Build Cache" name="ClearBuildCache" />
<ui:Toggle label="Use Asset Depend DB" name="UseAssetDependency" />
<ui:VisualElement name="PopupContainer" style="flex-grow: 1;" />
<uie:EnumField label="File Name Style" value="Center" name="FileNameStyle" />
<uie:EnumField label="Bundled Copy Option" value="Center" name="BundledCopyOption" />
<ui:TextField picking-mode="Ignore" label="Bundled Copy Param" name="BundledCopyParam" />
<ui:VisualElement name="ExtensionContainer" />
<ui:Button text="Click Build" display-tooltip-when-elided="true" name="Build" style="height: 50px; background-color: rgb(40, 106, 42); margin-top: 10px;" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 9bff4878063eaf04dab8713e1e662ac5 guid: 3a043220a198ef14c8656a51c520ccd3
ScriptedImporter: ScriptedImporter:
internalIDToNameTable: [] internalIDToNameTable: []
externalObjects: {} externalObjects: {}

View File

@@ -30,6 +30,11 @@ namespace YooAsset.Editor
/// </summary> /// </summary>
protected TextField _buildVersionField; protected TextField _buildVersionField;
/// <summary>
/// 构建资源包类型下拉框
/// </summary>
protected DropdownField _buildBundleTypeField;
public override void CreateView(VisualElement parent) public override void CreateView(VisualElement parent)
{ {
@@ -50,6 +55,10 @@ namespace YooAsset.Editor
_buildVersionField = Root.Q<TextField>("BuildVersion"); _buildVersionField = Root.Q<TextField>("BuildVersion");
SetBuildVersionField(_buildVersionField); SetBuildVersionField(_buildVersionField);
// 构建资源包类型
_buildBundleTypeField = Root.Q<DropdownField>("BuildBundleType");
SetBuildBundleTypeField(_buildBundleTypeField);
// 构建按钮 // 构建按钮
var buildButton = Root.Q<Button>("Build"); var buildButton = Root.Q<Button>("Build");
buildButton.clicked += BuildButton_clicked; buildButton.clicked += BuildButton_clicked;
@@ -80,7 +89,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = BundleBuilderHelper.GetDefaultBuildOutputRoot(); buildParameters.BuildOutputRoot = BundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BundledFileRoot = BundleBuilderHelper.GetStreamingAssetsRoot(); buildParameters.BundledFileRoot = BundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString(); buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBundleType.VirtualBundle; buildParameters.BuildBundleType = (int)Enum.Parse(typeof(EBundleType), _buildBundleTypeField.value);
buildParameters.BuildTarget = BuildTarget; buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName; buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value; buildParameters.PackageVersion = _buildVersionField.value;
@@ -94,5 +103,19 @@ namespace YooAsset.Editor
if (buildResult.Success) if (buildResult.Success)
EditorUtility.RevealInFinder(buildResult.OutputPackageDirectory); EditorUtility.RevealInFinder(buildResult.OutputPackageDirectory);
} }
private void SetBuildBundleTypeField(DropdownField dropdownField)
{
var bundleTypes = Enum.GetValues(typeof(EBundleType))
.Cast<EBundleType>()
.Where(type => type.ToString().StartsWith("Virtual"))
.Select(type => type.ToString())
.ToList();
dropdownField.choices = bundleTypes;
dropdownField.SetValueWithoutNotify(EBundleType.VirtualAssetBundle.ToString());
dropdownField.style.width = StyleWidth;
UIElementsTools.SetElementLabelMinWidth(dropdownField, LabelMinWidth);
}
} }
} }

View File

@@ -2,6 +2,7 @@
<ui:VisualElement name="BuildContainer"> <ui:VisualElement name="BuildContainer">
<ui:TextField picking-mode="Ignore" label="Build Output" name="BuildOutput" /> <ui:TextField picking-mode="Ignore" label="Build Output" name="BuildOutput" />
<ui:TextField picking-mode="Ignore" label="Build Version" name="BuildVersion" /> <ui:TextField picking-mode="Ignore" label="Build Version" name="BuildVersion" />
<ui:DropdownField label="Build Bundle Type" name="BuildBundleType" />
<ui:VisualElement name="ExtensionContainer" /> <ui:VisualElement name="ExtensionContainer" />
<ui:Button text="Click Build" display-tooltip-when-elided="true" name="Build" style="height: 50px; background-color: rgb(40, 106, 42); margin-top: 10px;" /> <ui:Button text="Click Build" display-tooltip-when-elided="true" name="Build" style="height: 50px; background-color: rgb(40, 106, 42); margin-top: 10px;" />
</ui:VisualElement> </ui:VisualElement>

View File

@@ -7,6 +7,7 @@ namespace YooAsset.Editor
[Serializable] [Serializable]
public class EditorAssetInfo : IComparable<EditorAssetInfo> public class EditorAssetInfo : IComparable<EditorAssetInfo>
{ {
[NonSerialized]
private string _fileExtension = null; private string _fileExtension = null;
/// <summary> /// <summary>

View File

@@ -118,28 +118,6 @@ namespace YooAsset.Editor
File.Copy(sourcePath, destPath, overwrite); File.Copy(sourcePath, destPath, overwrite);
} }
/// <summary>
/// 清空文件夹
/// </summary>
/// <param name="directoryPath">要清理的文件夹路径</param>
public static void ClearFolder(string directoryPath)
{
if (Directory.Exists(directoryPath) == false)
return;
string[] allFiles = Directory.GetFiles(directoryPath);
for (int i = 0; i < allFiles.Length; i++)
{
File.Delete(allFiles[i]);
}
string[] allFolders = Directory.GetDirectories(directoryPath);
for (int i = 0; i < allFolders.Length; i++)
{
Directory.Delete(allFolders[i], true);
}
}
/// <summary> /// <summary>
/// 获取文件字节大小 /// 获取文件字节大小
/// </summary> /// </summary>

View File

@@ -187,7 +187,7 @@
identification within third-party archives. identification within third-party archives.
Copyright 2018-2021 何冠峰 Copyright 2018-2021 何冠峰
Copyright 2021-2025 TuYoo Games Copyright 2021-2026 TuYoo Games
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -1,12 +1,12 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
// 内部友元 // 内部友元
[assembly: InternalsVisibleTo("YooAsset.Editor")] [assembly: InternalsVisibleTo("YooAsset.Editor")]
[assembly: InternalsVisibleTo("YooAsset.Test")] [assembly: InternalsVisibleTo("YooAsset.Tests")]
[assembly: InternalsVisibleTo("YooAsset.Test.Editor")] [assembly: InternalsVisibleTo("YooAsset.Tests.Editor")]
// 外部友元 // 外部友元
[assembly: InternalsVisibleTo("YooAsset.MiniGame")] [assembly: InternalsVisibleTo("YooAsset.MiniGame")]
[assembly: InternalsVisibleTo("YooAsset.RuntimeExtension")] [assembly: InternalsVisibleTo("YooAsset.Extension")]
[assembly: InternalsVisibleTo("YooAsset.EditorExtension")] [assembly: InternalsVisibleTo("YooAsset.Extension.Editor")]
[assembly: InternalsVisibleTo("Assembly-CSharp-Editor")] [assembly: InternalsVisibleTo("Assembly-CSharp-Editor")]

View File

@@ -8,7 +8,7 @@ namespace YooAsset
/// <summary> /// <summary>
/// 异步操作基类 /// 异步操作基类
/// </summary> /// </summary>
public abstract class AsyncOperationBase : IEnumerator, IComparable<AsyncOperationBase> public abstract partial class AsyncOperationBase : IEnumerator, IComparable<AsyncOperationBase>
{ {
private List<AsyncOperationBase> _children; private List<AsyncOperationBase> _children;
private Action<AsyncOperationBase> _completedCallback; private Action<AsyncOperationBase> _completedCallback;

View File

@@ -25,5 +25,13 @@ namespace YooAsset
/// 已失败 /// 已失败
/// </summary> /// </summary>
Failed, Failed,
#if YOOASSET_LEGACY_API
/// <summary>
/// v2.3 兼容别名
/// </summary>
[System.Obsolete("Use Succeeded instead.")]
Succeed = Succeeded,
#endif
} }
} }

View File

@@ -0,0 +1,99 @@
using System;
namespace YooAsset
{
/// <summary>
/// 从本地加载 ArchiveBundle 操作
/// </summary>
internal sealed class LoadLocalArchiveBundleOperation : BCLoadBundleOperation
{
private enum ESteps
{
None,
LoadBundle,
CheckResult,
Done,
}
private readonly LoadLocalArchiveBundleOptions _options;
private ArchiveBundle _archiveBundle;
private ESteps _steps = ESteps.None;
/// <summary>
/// 创建本地 ArchiveBundle 加载操作实例
/// </summary>
/// <param name="options">从本地加载 ArchiveBundle 的配置选项</param>
public LoadLocalArchiveBundleOperation(LoadLocalArchiveBundleOptions options)
{
_options = options;
}
protected override void InternalStart()
{
_steps = ESteps.LoadBundle;
}
protected override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.LoadBundle)
{
if (_options.Bundle.IsEncrypted)
{
_steps = ESteps.Done;
SetError($"ArchiveBundle encrypted loading is not supported: '{_options.FilePath}'.");
return;
}
if (FileUtility.IsFileIOSupported(_options.FilePath) == false)
{
_steps = ESteps.Done;
SetError($"FileIO is not supported for builtin path: '{_options.FilePath}'.");
return;
}
LoadResult result = ParseArchiveFile();
if (result.Succeeded == false)
{
_steps = ESteps.Done;
SetError(result.Error);
return;
}
_steps = ESteps.CheckResult;
}
if (_steps == ESteps.CheckResult)
{
if (_archiveBundle == null)
{
_steps = ESteps.Done;
SetError($"Loaded archive bundle is null.");
}
else
{
_steps = ESteps.Done;
SetResult();
BundleHandle = new ArchiveBundleHandle(_options.FilePath, _options.Bundle, _archiveBundle);
}
}
}
protected override void InternalWaitForCompletion()
{
ExecuteBatch();
}
private LoadResult ParseArchiveFile()
{
try
{
_archiveBundle = ArchiveBundleHelper.LoadArchiveBundle(_options.FilePath);
return LoadResult.Default();
}
catch (Exception ex)
{
return LoadResult.Failure($"Failed to parse archive file: {ex.Message}.");
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More