UVP 价值专家 | 主程必杀技:Unity C# 代码整洁之道(三)

文摘   游戏   2024-05-24 19:17   上海  
这篇文章来自 2024 年度 Unity 价值专家提名人选 Laugh。Laugh 专注游戏研发及管理 15 年,目前在巨人网络担任技术负责人、专家,负责 MMO 类型的架构设计、产线设计及团队建设。他在 Unity 官方开发者社区发布了“主程必杀技”系列文章,探索使用 Source Generator 和 Analyzer 在 Unity 中实现代码规范框架要求的方式,帮助主程更高效地进行技术管理。
点击阅读原文,可以访问 Laugh 在 Unity 官方开发者社区的个人主页,阅读更多技术干货。

在本系列的前两篇文章中,我们先通过开发 Analyzer 尝试了强制使用大括号包括逻辑分支 (if...else...) 的写法,又一起实现了强制加注释不符合要求的代码会直接报 Error 且无法编译,方便主程进行代码规范管理和框架要求,而不再需要花费大量时间 Review。

本文为大家带来 Laugh 的“主程必杀技”系列文章第三篇《Unity C# 自动化执行代码规范-命名规范检查》,包括检查命名空间和类名、public 和 private 方法名称、成员变量命名、临时变量命名。系列其他文章将每周更新,或可访问 Laugh 的 Unity 官方开发者社区主页,持续关注学习。

演示代码

获取链接:https://gitee.com/palang/unity-sharp-code-regulator.git

C# Code Analyzer 入门系列文章
 Analyzer 初体验:强制使用大括号包括逻辑分支 (if...else...)
 强制加注释(上)类注释 | 强制加注释(下)方法注释
 命名规范检查(一)命名空间和类名
 命名规范检查(二)public 和 private 方法名称规范检查
 命名规范检查(三)成员变量命名规范检查
 命名规范检查(四)临时变量命名检查

 检查继承关系

Unity C# 自动化执行代码规范

命名规范检查

看本篇文章之前,请先阅读第一篇教程,环境准备工作也与前两篇保持一致。

环境准备

1. VS 2022
2. 版本:团结引擎 1.0
3. Analyzer 库项目类库框架选择 .Net Standard 2.0 (兼容 Unity) 

4. Microsoft.CodeAnalysis.CSharp,安装 3.8.0 版本 基本过程可以参考一篇文章

命名空间和类名检查

命名空间和类名一般要求驼峰命名法,我们来检查者两者是否符合规范。但驼峰命名要检查每个单词,需要英文单词库,需要的可以自行建立单词库,这边只检查首字母是否大写和是否全是大写两种情况。

  实现目标:

1.强制类名首字母大写,并且不能全部是大写; 

2.强制命名空间每段首字母大写

  编写 Analyzer 分析器:

首先在类 DianogsticIDs 中定义新的错误 ID:
public const string FORCE_NAMING_CONVENTIONS_ID= "FERR1003";`
然后在 AnalyzerReleases.Unshipped.md 文件添加该规则,如下:
新建分析器类 ForceNSClassNamingConventions.cs,内容如下(具体步骤请看代码中的注释)
using Analyzer;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis.Diagnostics;using System;using System.Collections.Generic;using System.Collections.Immutable;using System.Linq;using System.Text;
namespace CdeAnalyzer{ /** * Author: Laugh(笑微) * https://developer.unity.cn/projects/65937455edbc2a001cbd8102 */ [DiagnosticAnalyzer(LanguageNames.CSharp)] internal class ForceNSClassNamingConventions : DiagnosticAnalyzer { /// <summary> /// 错误描述 /// </summary> private static readonly DiagnosticDescriptor ForceNamingConventionsDescriptor = new DiagnosticDescriptor( DianogsticIDs.FORCE_NAMING_CONVENTIONS_ID, // ID "命名空间或类名不符合规范", // Title "命名空间或类名不符合规范", // Message format DiagnosticCategories.Criterion, // Category DiagnosticSeverity.Error, // Severity isEnabledByDefault: true // Enabled by default ); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(ForceNamingConventionsDescriptor);
public override void Initialize(AnalysisContext context) { context.RegisterSyntaxTreeAction(AnalyzeSymbol); }
private static void AnalyzeSymbol(SyntaxTreeAnalysisContext context) { //找到文档的语法根树 var root = context.Tree.GetRoot(context.CancellationToken);
var classNodeList = root.DescendantNodes()?.OfType<ClassDeclarationSyntax>(); foreach (var cls in classNodeList) { var clsName = cls.Identifier.ToString(); var firstChar = clsName.First().ToString(); //如果全是小写或全是大写或首字母非大写,则不符合驼峰命名法(粗略检查),复杂的规矩可以自行定义 if (clsName == clsName.ToLower() || clsName == clsName.ToUpper() || firstChar != firstChar.ToUpper() ) { //报错 var diagnostic = Diagnostic.Create(ForceNamingConventionsDescriptor, cls.GetFirstToken().GetLocation()); context.ReportDiagnostic(diagnostic); } }
var nsNodeList = root.DescendantNodes()?.OfType<NamespaceDeclarationSyntax>(); foreach(var ns in nsNodeList) { var nsName = ns.Name.ToString(); //拆分命名空间的级段 var nlist = nsName.Split(new char[] { '.' }); foreach(var n in nlist) { var firstChar = n.First().ToString(); //如果首字母非大写,则不符合驼峰命名法(粗略检查),复杂的规矩可以自行定义 if (firstChar != firstChar.ToUpper() ) { //报错 var diagnostic = Diagnostic.Create(ForceNamingConventionsDescriptor, ns.GetFirstToken().GetLocation()); context.ReportDiagnostic(diagnostic); break; } } } } }}
代码可以直接使用。

public 和 private 方法名称规范检查

方法命名与类名比较类似,一般是大驼峰(pascal)或小驼峰,有时候些使用下划线分割。 我们还是以驼峰命名为例来探讨。

  实现目标:

1. public 方法使用大驼峰命名法,并且不能全是大写;
2. 非 public 方法使用小驼峰命名法,并且不能全是小写(本规则不合理,只是来演示功能);

3. 添加排除检查的目录和类文件

  定义排除工具类:

主要排除指定目录和类文件,定义如下:
using System;using System.Collections.Generic;using System.IO;using System.Text;
namespace Analyzer{ public class ConstraintDefinition { /// <summary> /// 检查时排除的目录 /// </summary> static List<string> AnalyzerExcludePath = new List<string>() { "/PackageCache/", "/ThirdLibs/", "/Plugins/" }; /// <summary> /// 检查时排除的文件 /// </summary> static List<string> AnalyzerExcludeFileName = new List<string>() { "Program.cs" }; /// <summary> /// 是否是需要排除检查 /// </summary> /// <param name="path"></param> /// <returns></returns> public static bool ExcludeAnalize(string path) { var fileName = Path.GetFileName(path); if (AnalyzerExcludeFileName.Contains(fileName)) { return true; } foreach(var file in AnalyzerExcludePath) { if(path.Contains(file)) { return true; } } return false; } }}
  编写 Analyzer 分析器:

命名规则使用同一个错误 ID:

public const string FORCE\_NAMING\_CONVENTIONS\_ID= "FERR1003";

新建分析器类 ForceFunctionNameConventions.cs,内容如下(具体步骤请看代码中的注释)

using Analyzer; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text;
namespace CdeAnalyzer { /**     * Author: Laugh(笑微) * https://developer.unity.cn/projects/65937455edbc2a001cbd8102 */ [DiagnosticAnalyzer(LanguageNames.CSharp)] internal class ForceFunctionNameConventions : DiagnosticAnalyzer { /// <summary> /// 错误描述 /// </summary> private static readonly DiagnosticDescriptor PublicFunDescriptor = new DiagnosticDescriptor( DianogsticIDs.FORCE_NAMING_CONVENTIONS_ID, // ID "public 方法名不符合规范", // Title "public 方法名不符合规范", // Message format DiagnosticCategories.Criterion, // Category DiagnosticSeverity.Error, // Severity isEnabledByDefault: true // Enabled by default );
private static readonly DiagnosticDescriptor PrivateFunDescriptor = new DiagnosticDescriptor( DianogsticIDs.FORCE_NAMING_CONVENTIONS_ID, // ID "private 方法名不符合规范", // Title "private 方法名不符合规范", // Message format DiagnosticCategories.Criterion, // Category DiagnosticSeverity.Error, // Severity isEnabledByDefault: true // Enabled by default ); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(PublicFunDescriptor, PrivateFunDescriptor);

public override void Initialize(AnalysisContext context) { context.RegisterSyntaxTreeAction(AnalyzeSymbol); }
private static void AnalyzeSymbol(SyntaxTreeAnalysisContext context) { //找到文档的语法根树 var root = context.Tree.GetRoot(context.CancellationToken); if (ConstraintDefinition.ExcludeAnalize(context.Tree.FilePath)) {//排除特殊目录 return; } var methodNodeList = root.DescendantNodes()?.OfType<MethodDeclarationSyntax>();            foreach (var method in methodNodeList) { var clsName = method.Identifier.ToString(); var firstChar = clsName.First().ToString(); var tokens = method.ChildTokens(); foreach ( var token in tokens) { //public 方法:首字母大写,大驼峰命名法(pascal) if (token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PublicKeyword)) { //全是大写或首字母非大写,则不符合大驼峰命名法(粗略检查),复杂的规矩可以自行定义 if (clsName == clsName.ToLower() || clsName == clsName.ToUpper() || firstChar != firstChar.ToUpper() ) { //报错 var diagnostic = Diagnostic.Create(PublicFunDescriptor, method.GetFirstToken().GetLocation()); context.ReportDiagnostic(diagnostic); } break;//只检查一次 } else if (token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PrivateKeyword) || token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.ProtectedKeyword) || token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.InternalKeyword) || token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.IdentifierToken)) { //其他:private protected 等等,使用小驼峰命名法 //首字母小写 if (firstChar != firstChar.ToLower() ) { //报错 var diagnostic = Diagnostic.Create(PrivateFunDescriptor, method.GetFirstToken().GetLocation()); context.ReportDiagnostic(diagnostic); } break;//只检查一次 } }
} } }
}

代码修改后再使用。

成员变量命名规范检查

类的成员变量,每个项目要求不尽相同。 一般情况下,private、protected 变量要求以下划线开头,不能包含 public 变量(避免被外部默默修改)。以此为例,我们来一起学习一下对成员变量的强制规范。 本规范只为演示。
  实现目标:

1. 强制 private protected 变量以下划线开头;

2. 强制类中不能包含 public 的成员变量
  编写 Analyzer 分析器:

命名规则使用同一个错误 ID:

public const string FORCE\_NAMING\_CONVENTIONS\_ID= "FERR1003";

定义分析器类 ForceMemberVariableConventions.cs,内容如下:

using Analyzer;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis.Diagnostics;using System;using System.Collections.Generic;using System.Collections.Immutable;using System.Linq;using System.Text;
namespace CdeAnalyzer{ /** * Author: Laugh(笑微) * https://developer.unity.cn/projects/65937455edbc2a001cbd8102 */ [DiagnosticAnalyzer(LanguageNames.CSharp)] internal class ForceMemberVariableConventions : DiagnosticAnalyzer { /// <summary> /// 错误描述 /// </summary> private static readonly DiagnosticDescriptor PublicVarDescriptor = new DiagnosticDescriptor( DianogsticIDs.FORCE_NAMING_CONVENTIONS_ID, // ID "类中不不能定义共有变量,请使用Getter Setter 或方法", // Title "类中不不能定义共有变量,请使用Getter Setter 或方法", // Message format DiagnosticCategories.Criterion, // Category DiagnosticSeverity.Error, // Severity isEnabledByDefault: true // Enabled by default );
private static readonly DiagnosticDescriptor PrivateVarDescriptor = new DiagnosticDescriptor( DianogsticIDs.FORCE_NAMING_CONVENTIONS_ID, // ID "private 变量名名不符合规范,必须以下划线(_)开头的小驼峰命名", // Title "private 变量名名不符合规范,必须以下划线(_)开头的小驼峰命名", // Message format DiagnosticCategories.Criterion, // Category DiagnosticSeverity.Error, // Severity isEnabledByDefault: true // Enabled by default ); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(PublicVarDescriptor, PrivateVarDescriptor);
public override void Initialize(AnalysisContext context) { context.RegisterSyntaxTreeAction(AnalyzeSymbol); }
private static void AnalyzeSymbol(SyntaxTreeAnalysisContext context) { //找到文档的语法根树 var root = context.Tree.GetRoot(context.CancellationToken); if (ConstraintDefinition.ExcludeAnalize(context.Tree.FilePath)) {//排除特殊目录 return; } var fieldNodeList = root.DescendantNodes()?.OfType<FieldDeclarationSyntax>(); foreach (var field in fieldNodeList) { var filedName = field.Declaration.Variables.ToString(); var firstChar = filedName.First().ToString(); var tokens = field.ChildTokens(); foreach (var token in tokens) { //不能包含Public 变量 if (token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PublicKeyword)) { //报错 var diagnostic = Diagnostic.Create(PublicVarDescriptor, field.GetFirstToken().GetLocation()); context.ReportDiagnostic(diagnostic); break;//只检查一次 } else if (token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PrivateKeyword) || token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.ProtectedKeyword) || token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.InternalKeyword) || token.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.IdentifierToken)) { //其他:private protected 等等,使用_开头的小驼峰命名法 if (firstChar != "_" || filedName == filedName.ToUpper()) { //报错 var diagnostic = Diagnostic.Create(PrivateVarDescriptor, field.GetFirstToken().GetLocation()); context.ReportDiagnostic(diagnostic); } break;//只检查一次 } }
} } }}

临时变量命名规范检查

为了区分临时变量和成员变量,临时变量的命名与成员变量需要做一些区别,演示实例中使用小写字母开头的小驼峰命名法,并且不能以下划线开头。
  实现目标:

1. 强制临时变量以小写字母开头;

2. 强制不能以下划线开头(和私有成员变量作区分)
  编写 Analyzer 分析器:

命名规则使用同一个错误 ID:

public const string FORCE\_NAMING\_CONVENTIONS\_ID= "FERR1003";
定义分析器类 ForceLocalVariableConventions.cs,内容如下
using Analyzer;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis.Diagnostics;using System;using System.Collections.Generic;using System.Collections.Immutable;using System.Linq;using System.Text;
namespace CdeAnalyzer { /** * Author: Laugh(笑微) * https://developer.unity.cn/projects/65937455edbc2a001cbd8102 */ [DiagnosticAnalyzer(LanguageNames.CSharp)] internal class ForceLocalVariableConventions : DiagnosticAnalyzer { /// <summary> /// 错误描述 /// </summary> private static readonly DiagnosticDescriptor LocalVarDescriptor = new DiagnosticDescriptor( DianogsticIDs.FORCE_NAMING_CONVENTIONS_ID, // ID "临时变量命名不符合规范,请使用以字母开头的小驼峰命名法", // Title "临时变量命名不符合规范,请使用以字母开头的小驼峰命名法", // Message format DiagnosticCategories.Criterion, // Category DiagnosticSeverity.Error, // Severity isEnabledByDefault: true // Enabled by default ); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(LocalVarDescriptor);
public override void Initialize(AnalysisContext context) { context.RegisterSyntaxTreeAction(AnalyzeSymbol); } private static void AnalyzeSymbol(SyntaxTreeAnalysisContext context) { //找到文档的语法根树 var root = context.Tree.GetRoot(context.CancellationToken); if (ConstraintDefinition.ExcludeAnalize(context.Tree.FilePath)) {//排除特殊目录 return; } var localNodeList = root.DescendantNodes()?.OfType<LocalDeclarationStatementSyntax>(); foreach(var localNode in localNodeList) { var varList = localNode.Declaration.Variables; foreach(var localVar in varList) { var localName = localVar.Identifier.Value.ToString(); var firstChar = localName.First().ToString(); if (firstChar.ToLower() != firstChar) {//判断第一个字母是否是小写 //报错 var diagnostic = Diagnostic.Create(LocalVarDescriptor, localNode.GetFirstToken().GetLocation()); context.ReportDiagnostic(diagnostic); } }
} } }}


下周将带来 Laugh 的“主程必杀技” Unity C# 代码整洁之道系列文章第四辑《检查继承关系》,敬请期待~
Laugh 是 2024 年度 Unity 价值专家提名人选。Unity 价值专家(UVP)是通过原创作品启发国内创作者的 Unity 专业人员,点击这里提名/自荐
长按关注
Unity 官方开发者服务平台
第一时间了解 Unity 社区动向,学习开发技巧

 点击“阅读原文”,访问 Laugh 的社区主页 



Unity官方开发者服务平台
Unity引擎官方开发者服务平台,分享技术干货、学习课程、产品信息、前沿案例、活动资讯、直播信息等内容。
 最新文章