C#编程风格指南
C# 编程风格指南!
编写简洁、可扩展的代码
简介
创造力可能会很混乱。
闪现的灵感变成了一连串的代码,然后产生了一个工作原型。成功!恭喜您通过了第一个障碍。但是,仅仅让您的代码正常工作是不够的。游戏开发还有更多内容。
一旦你的逻辑正常运行,那么重构和清理的过程就开始了。
本指南汇集了行业专家关于如何编写代码规范的建议。为团队的每个成员建立这样的指南,有助于确保您的代码库可以将项目发展到商业规模的生产。
从长远来看,这些提示和技巧将对您的开发过程有所帮助,即使它们会花费您额外的精力。更简洁、更具可扩展性的代码库还有助于在扩展团队时高效地引导新开发人员。
保持代码干净,让您自己和参与项目的每个人都更轻松。
贡献
本指南由 Wilmer Lin 编写,Wilmer Lin 是一位 3D 和视觉效果艺术家,在电影和电视领域拥有超过 15 年的行业经验,现在是一名独立的游戏开发人员和教育工作者。高级技术内容营销经理 Thomas Krogh-Jacobsen 和 Unity 高级工程师 Peter Andreasen、Scott Bilas 和 Robert LaCruise 也做出了重大贡献。
什么是干净的代码
“干净的代码总是看起来像是由在意干净代码的人编写的”-Michael Feathers《Working Effectively with Legacy Code》作者
大多数游戏开发人员都会同意,干净的代码是任何易于阅读和维护的代码。
干净的代码优雅、高效且可读。
这种一致性是有充分理由的。对于作为原始作者来说可能很明显的事情,对于其他开发人员来说可能不太明显。出于同样的原因,当你现在实现一些逻辑时,你可能不记得三个月后相同的代码片段做了什么。
简洁代码旨在使开发更具可扩展性并符合一组生产标准,包括:
- 遵循一致的命名约定
- 格式化代码以提高可读性
- 组织类和方法以保持其小巧易读
- 对任何不便于理解的代码进行注释
无论您是在构建移动益智游戏还是针对游戏机的大型 MMORPG,保持代码库的整洁都可以降低软件维护的总成本。然后,您可以更轻松地开发新功能或修补现有软件。
你未来的队友,以及你未来的自己,会为此心存感激。
团队发展
“任何傻瓜都可以编写代码计算机可以理解。优秀的程序员编写代码人类可以理解的”-Martin Fowler,《Refactorin》 的作者
没有开发商是一座孤岛。随着游戏应用程序技术需求的增长,您将需要帮助。不可避免地,您将添加更多具有不同技能组合的团队成员。Clean Code 为您不断扩展的团队引入了编码标准,以便所有人统一规范。现在,每个人都可以使用一套更统一的指导方针来处理同一个项目。
在研究如何创建样式指南之前,让我们先了解一些一般规则,以帮助您扩展 Unity 开发。
KISS (保持简单,愚蠢)
让我们面对现实吧:工程师和开发人员可能会使事情变得过于复杂,即使计算和编程已经够难了。以 “keep it simple, stupid” 的 KISS 原则为指导,为手头的问题找到最简单的解决方案。
如果一种行之有效的简单技术解决了您的挑战,则无需重新发明轮子。为什么要为了使用而使用花哨的新技术呢?Unity 的脚本 API 中已经包含了许多解决方案。例如,如果现有的 Hexagonal Tilemap 适用于您的策略游戏,请跳过编写自己的 Tilemap。您可以编写的最好的代码是完全没有代码。
KISS 原则
众所周知的 KISS 原则强调设计的简单性,这一理念在不同时期都很流行,正如这些引述所证明的那样:
- ”简单是终极的精致。“-Leonardo da Vinci
- ”让简单的任务变得简单!“-Bjarne Stroustrup
- “简单性是可靠性的先决条件。”-– Edsger W. Dijkstra
- “一切都应该尽可能简单,但不能更简单。”-Albert Einstein
在编程中,这意味着要尽可能简化代码。避免增加不必要的复杂性。
YAGNI 原则
相关的 YAGNI 原则(“您不会需要它”)指示您仅在需要时实现功能。不用担心项目完备时(once the stars align)可能需要的功能。构建您现在需要的最简单的东西,并构建它以使其正常工作。
不要围绕问题进行编码
软件开发的第一步是了解您要解决的问题。这个想法似乎是常识,但开发人员经常在不了解实际问题的情况下陷入实现代码的困境,或者他们会修改代码直到它工作,而没有完全理解原因。
例如,如果您在方法顶部使用快速 if-null 语句修复了 Null 引用异常,该怎么办。您确定这是真正的罪魁祸首,还是问题在于内部更深处调用了另一种方法?
不要添加代码来解决问题,而是调查根本原因。问问自己为什么会这样,而不是使用创可贴解决方案。
每天逐步改进
制作干净的代码是一个流畅且持续的过程。让整个团队都进入这种心态。代码清理将成为您作为开发人员日常生活的一部分。大多数人不打算编写损坏的代码。它只是随着时间的推移而演变。您的代码库需要不断维护和保养。为此预算时间并确保它发生。
让它变得美好,而不是完美
另一方面,不要追求完美。当您的代码满足生产标准时,就可以提交它并继续前进了。
最终,您的代码需要执行一些操作。在实现新功能与代码清理之间取得平衡。不要为了它而重构。当你认为它会为你或其他人带来好处时,进行重构。
规划,但适应
在《The Pragmatic Programmer》中,Andy Hunt 和 Dave Thomas 写道,“与其说是构建,不如说编程更像是园艺。软件工程是一个有机的过程。如果一切没有按计划进行,请做好准备”。
即使你画了最精致的图纸,在纸上设计一个花园也不能保证结果。您的植物开花时间可能与您预期的不同。您需要修剪、移植和替换部分代码才能使这个花园成功。
软件设计不太像建筑师绘制蓝图,因为它更具延展性,而且不那么机械化。您需要随着代码库的增长做出反应。
保持一致
一旦你决定如何解决问题,就用同样的方式处理类似的事情。这并不困难,但需要不断的努力。将此原则应用于从命名(类和方法、大小写等)到组织项目文件夹和资源的所有内容。
最重要的是,让您的团队就风格指南达成一致,然后遵循它。
众木成林
尽管保持代码简洁符合每个人的最佳利益,但“干净简单”并不等同于“简单”。干净简单需要付出努力,对于初学者和经验丰富的开发人员来说都是一项艰巨的工作。
如果不加以控制,您的项目将变得混乱。这是这么多人在项目的不同部分工作的自然结果。每个人都有责任参与并防止代码混乱,每个团队成员都需要阅读并遵循样式指南。清理是一项团队工作。
适合您和您的团队的风格指南
本指南重点介绍您在 Unity 开发过程中会遇到的最常见的编码约定。这些是 Microsoft 框架设计准则的子集,其中包括除此处介绍的内容之外的大量规则。
这些准则是建议,而不是硬性规定。根据您团队的偏好自定义它们。选择适合每个人的风格,并确保他们应用它。
一致性为王。如果您遵循这些建议,并且将来需要修改您的样式指南,则一些查找和替换操作可以快速迁移您的代码库。
当您的样式指南与本文档或 Microsoft 框架设计指南冲突时,应优先于它们,因为这将允许您的团队在整个项目中保持统一的样式。
编码风格指南
“计算机科学中只有两件难的事情:缓存失效和命名事物”-– Phil Karlton,软件工程师
您的应用程序是可能思维方式不同的个人的集体产物。风格指南有助于控制这些差异,以创建一个有凝聚力的最终产品。无论有多少贡献者参与一个 Unity 项目,都应该感觉它是由单个作者开发的。
Microsoft 和 Google 都提供了全面的示例指南:
这些是管理 Unity 开发的绝佳起点。每个指南都提供了命名、格式化和注释的解决方案。如果你是一个独立的开发人员,一开始可能会觉得这是一个限制,但在团队中工作时,遵循风格指南是必不可少的。
将风格指南视为一项初始投资,以后会带来回报。如果您将任何人转移到另一个项目,保持一组标准可以减少重新学习所花费的时间。
样式指南消除了编码约定和格式设置中的猜测。因此,一致的风格就变成了遵循方向的问题。
我们创建了一个Unity-Code-Style-Guide,您也可以在编写自己的指南时将其用作参考。请根据需要随意复制和调整它。
让我们开始吧。
命名约定
给某物起个名字涉及一种深奥的心理。名称告诉我们该实体如何适应世界。这是什么?谁?它能为我们做什么?
变量、类和方法的名称不仅仅是标签。它们承载着重量和意义。好的命名风格会影响阅读您的程序的人如何理解您试图传达的想法。
以下是一些命名要遵循的准则。
标识符名称
标识符是分配给类型 (类、接口、结构、委托或枚举) 、成员、变量或命名空间的任何名称。标识符必须以字母或下划线 (_) 开头。
避免在标识符中使用特殊字符(反斜杠、符号、Unicode 字符),即使 C# 允许它们。这些可能会干扰某些 Unity 命令行工具。避免使用不常见的字符,以确保与大多数平台兼容。
大小写术语
不能定义名称中包含空格的变量,因为 C# 使用空格字符来分隔标识符。大小写方案可以缓解在源代码中使用复合名称或短语的问题。有几种众所周知的命名和大小写约定。
Camel case
驼峰式大小写也称为驼峰式大写,是写短语而不带空格或标点符号的做法,用单个大写字母分隔单词。第一个字母是小写的。局部变量和方法参数是驼峰式大小写。
例如:
- examplePlayerController
- maxHealthPoints
- endOfFile
Pascal case
Pascal 大小写是 camel 大小写的变体,其中首字母大写。在 Unity 开发中将其用于类和方法名称。Public 字段也可以是 pascal 大小写。例如:
- ExamplePlayerController
- MaxHealthPoints
- EndOfFile
Snake case
在这种情况下,单词之间的空格将替换为下划线字符。例如:
- example_player_controller
- max_health_points
- end_of_file
Kebab case
在这里,单词之间的空格被破折号取代。这些单词显示在破折号字符的 “串” 上。例如:
- example-player-controller
- max-health-points
- end-of-file
- naming-conventions-methodology
kebab 大小写的问题在于许多编程语言使用破折号作为减号。某些语言将用短划线分隔的数字解释为日历日期。
Hungarian notation
变量或函数名称通常表示其意图或类型。例如:
- int iCounter
- string strPlayerName
字段和变量
请为变量和字段使用以下规则:
- 对变量名称使用名词:变量名称必须具有描述性、清晰且明确,因为它们表示事物或状态。因此,在命名它们时使用名词,除非变量是 bool 类型(见下文)
- 在布尔值前加上动词:这些变量表示 true 或 false 值。通常,它们是问题的答案,例如 – 玩家是否在跑步?游戏结束了吗?在它们前面加上一个动词,以使其含义更加明显。这通常与描述或条件配对,例如 isDead、isWalking、hasDamageMultiplier 等。
- 使用有意义的名称。不要缩写(除非是数学):您的变量名称将揭示它们的意图。选择易于发音和搜索的名称。
- 单字母变量对于循环和数学表达式很好,但除此之外,不要缩写。清晰度比省略几个元音所节省的任何时间都更重要。
- 在进行快速原型设计时,您可以使用简短的 “垃圾” 名称,然后在以后重构为有意义的名称。
要避免的示例 | 请改用 | 注意事项 |
---|---|---|
int d | int elapsedTimeInDays | 避免使用单个字母的缩写,除非是计数或 数学表达式。 |
int hp, string tName, int mvmtSpeed | int healthPoints, string teamName, int movementSpeed | 变量名称揭示了意图。使名称可搜索和发音。 |
int getMovemementSpeed | int movementSpeed | 使用名词。为方法保留动词,除非它是 bool (下面) |
bool dead | bool isDead bool isPlayerDead | 布尔值提出的问题可以是 true 或 false。 |
-
对公共字段使用 pascal 大小写。对私有变量使用驼峰式大小写:对于公共字段的替代方法,请使用带有公共 getter 的 Properties(请参阅下面的格式)。
-
避免过多的前缀或特殊编码:您可以在私有成员变量前面加上下划线 (),以区别于局部变量。或者,使用 this 关键字在上下文中区分成员变量和局部变量,并跳过前缀。公共字段和属性通常没有前缀。某些风格指南对私有成员变量 (m)、常量 (k_) 或静态变量 (s_) 使用前缀,因此名称可以一目了然地显示有关变量的更多信息。许多开发人员避开这些,而是依赖编辑器。但是,并非所有 IDE 都支持突出显示和颜色编码,并且某些工具根本无法显示丰富的上下文。在决定如何(或是否)作为一个团队一起应用前缀时,请考虑这一点。
-
始终指定(或省略)访问级别修饰符:如果省略访问修饰符,编译器将假定访问级别为 private。这效果很好,但在省略 default 访问修饰符的方式上要保持一致。请记住,如果以后想在子类中使用 protected,则需要使用 protected。
*** 示例代码片段***
本指南中的代码片段是非功能性的,并且是缩写的。它们在此处显示是为了显示样式和格式。
您还可以参考此示例Unity-Code-Style-Guide,该样式表基于 Microsoft 框架设计指南的修改版本,面向 Unity 开发人员。这只是如何设置团队风格指南的一个示例。
请注意这些代码示例中的特定样式规则:
- 默认的 private 访问修饰符未省略。
- 公共成员变量使用 pascal 大小写。
- 私有成员变量采用驼峰式大小写,并使用下划线 (_) 作为前缀。
- 局部变量和参数使用不带前缀的驼峰式大小写。
- Public 和 private 成员变量组合在一起。
查看示例样式指南中的每个规则,并根据团队的偏好对其进行自定义。单个规则的细节不如让每个人都同意始终如一地遵守它重要。如有疑问,请依靠您团队自己的指南来解决任何风格分歧。
// EXAMPLE: public and private variables public float DamageMultiplier = 1.5f; public float MaxHealth; public bool IsInvincible; private bool _isDead; private float _currentHealth; // parameters public void InflictDamage(float damage, bool isSpecialDamage) { // local variable int totalDamage = damage; // local variable versus public member variable if (isSpecialDamage) { totalDamage *= DamageMultiplier; } // local variable versus private member variable if (totalDamage > _currentHealth) { /// ... } }
- 每行使用一个变量声明:它不那么紧凑,但增强了可读性.
- 避免冗余名称:如果您的类名为 Player,则无需创建名为 PlayerScore 或 PlayerTarget 的成员变量。将它们修剪为 Score 或 Target。
- 避免使用笑话或双关语:虽然它们现在可能会引起人们的笑声,但 infiniteMonkeys 或 dudeWheresMyChar 变量在读取几十次后就站不住了。
- 如果 var 关键字有助于提高可读性并且类型很明显,则对隐式类型的局部变量使用 var 关键字:在样式指南中指定何时使用 var。例如,许多开发人员在隐藏变量的类型或循环外部的基元类型时避免使用 var。通常,当 var 使代码更易于阅读(例如,具有较长的类型名称)并且类型没有歧义时,请使用 var。
// EXAMPLE: good use of var var powerUps = new List<PowerUps>(); var dictionary = new Dictionary<string, List<GameObject>>(); // AVOID: potential ambiguity var powerUps = PowerUpManager.GetPowerUps()
枚举
枚举是由一组命名常量定义的特殊值类型。默认情况下,常量是整数,从 0 开始计数。
对枚举名称和值使用 pascal 大小写。您可以将公共枚举放在类之外,以使其全局。对枚举名称使用单数名词。
注意:标有 System.FlagsAttribute 属性的按位枚举是此规则的例外。通常将它们复数化,因为它们表示多个类型。
// EXAMPLE: enums use singular nouns
public enum WeaponType
{
Knife,
Gun,
RocketLauncher,
BFG
}
public enum FireMode
{
None = 0,
Single = 5,
Burst = 7,
Auto = 8,
}
// EXAMPLE: but a bitwise enum is plural
[Flags]
public enum AttackModes
{
// Decimal // Binary
None = 0, // 000000
Melee = 1, // 000001
Ranged = 2, // 000010
Special = 4, // 000100
MeleeAndSpecial = Melee | Special // 000101
}
类和接口
在命名类和接口时,请遵循以下标准规则:
- 对类名使用 pascal 大小写名词.
- 如果文件中有 Monobehaviour,则源文件名必须匹配:文件中可能有其他内部类,但每个文件只能存在一个 Monobehaviour。
- 在接口名称前加上大写字母 I:后跟一个描述功能的形容词。
// EXAMPLE: Class formatting
public class ExampleClass : MonoBehaviour
{
public int PublicField;
public static int MyStaticField;
private int _packagePrivate;
private int _myPrivate;
private static int _myPrivate;
protected int _myProtected;
public void DoSomething()
{
}
}
// EXAMPLE: Interfaces
public interface IKillable
{
void Kill();
}
public interface IDamageable<T>
{
void Damage(T damageTaken);
}
方法
在 C# 中,每条执行的指令都在方法的上下文中执行。
注意:“function”和“method”在 Unity 开发中经常互换使用。但是,由于如果不将函数合并到 C# 中的类中,则无法编写函数,因此“方法”是公认的术语。
方法执行操作,因此请应用这些规则来相应地命名它们:
- 以动词开头名称:如有必要,请添加上下文。例如,GetDirection、FindTarget 等。
- 对参数使用驼峰式大小写:将传入方法的参数格式设置为局部变量。
- 返回 bool 的方法应该提出问题:与布尔变量本身非常相似,如果方法返回 true-false 条件,则在方法前面加上动词 这以问题的形式表达它们,例如 IsGameOver、HasStartedTurn。
public void SetInitialPosition(float x, float y, float z)
{
transform.position = new Vector3(x, y, z);
}
// EXAMPLE: Methods ask a question when they return bool
public bool IsNewPosition(Vector3 currentPosition)
{
return (transform.position == newPosition);
}
事件和句柄
C# 中的事件实现 Observer 模式。此软件设计模式定义了一种关系,在该关系中,一个对象(主题 (或发布者) 可以通知称为观察者 (或订阅者) 的依赖对象列表。因此,主体可以向其观察者广播状态变化,而无需紧密耦合所涉及的对象。
主题和观察者中的事件及其相关方法存在多种命名方案。请尝试以下做法:
- 使用动词短语命名事件:选取能够准确传达状态更改的名称。使用现在分词或过去分词表示事件 “before” 或 “after”。例如,在开门之前为事件指定 “OpeningDoor”,或为之后的事件指定 “DoorOpened”。
- 将 System.Action 委托用于事件:在大多数情况下,Action
委托可以处理游戏所需的事件。您可以传递 0 到 16 个不同类型的输入参数,返回类型为 void。使用预定义的委托可以节省代码。
注意:您还可以使用 EventHandler 或 EventHandler
// EXAMPLE: Events
// using System.Action delegate
public event Action OpeningDoor; // event before
public event Action DoorOpened; // event after
public event Action<int> PointsScored;
public event Action<CustomEventArgs> ThingHappened;
- 在事件引发方法(在主题中)加上“On”前缀:调用事件的主题通常从前缀为“On”的方法执行此操作,例如“OnOpeningDoor”或“OnDoorOpened”。
// raises the Event if you have subscribers
public void OnDoorOpened()
{
DoorOpened?.Invoke();
}
public void OnPointsScored(int points)
{
PointsScored?.Invoke(points);
}
- 在事件处理方法(在观察者中)加上主题的名称和下划线 (_):如果主题名为“GameEvents”,则观察者可以使用名为“GameEvents_OpeningDoor”或“GameEvents_DoorOpened”的方法。请注意,这称为“事件处理方法”,不要与 EventHandler 委托混淆。为您的团队确定一致的命名方案,并在您的样式指南中实施这些规则。
- 仅在需要时创建自定义 EventArgs:如果需要将自定义数据传递给 Event,请创建一个新类型的 EventArgs,该类型可以从 System.EventArgs 或自定义结构继承。
// define an EventArgs if needed
// EXAMPLE: read-only, custom struct used to pass an ID and Color
public struct CustomEventArgs
{
public int ObjectID { get; }
public Color Color { get; }
public CustomEventArgs(int objectId, Color color)
{
this.ObjectID = objectId;
this.Color = color;
}
}
命名空间
使用 a 命名空间来确保你的类、接口、枚举等不会与其他命名空间或全局命名空间中的现有类、接口、枚举等冲突。命名空间还可以防止与 Asset Store 中的第三方资源发生冲突。
应用命名空间时:
- 使用不带特殊符号或下划线的 pascal 大小写
- 在文件顶部添加 using 指令,以避免重复键入命名空间前缀。
- 同时创建子命名空间。使用 dot(.) 运算符分隔名称级别,从而允许您将脚本组织到分层类别中。例如,您可以创建 MyApplication.GameFlow、MyApplication.AI、MyApplication.UI 等来保存游戏的不同逻辑组件。
namespace Enemy
{
public class Controller1 : MonoBehaviour
{
...
}
public class Controller2 : MonoBehaviour
{
...
}
}
在代码中,这些类称为 Enemy.Controller1 和 Enemy。Controller2 的 Controller2 中。添加 using 行以保存键入前缀:
当编译器找到类名 Controller1 和 Controller2 时,它会理解您指的是 Enemy.Controller1 和 Enemy.Controller2。
using Enemy;
如果脚本需要引用来自不同命名空间的同名类,请使用前缀来区分它们。例如,如果在 Player 命名空间中有 Controller1 和 Controller2 类,则可以写出 Player.Controller1 和 Player.Controller2 以避免任何冲突。否则,编译器将报告错误。
格式
“如果您希望代码易于编写,请使其易于阅读。”– Robert C. Martin《Clean Code and Agile Software Development》作者
除了命名之外,格式设置还有助于减少猜测并提高代码清晰度。通过遵循标准化的样式指南,代码审查不再是关于代码的外观,而是更多地关注它的作用。
在构建风格指南时,请个性化您的团队将如何格式化您的代码。在设置 Unity 开发风格指南时,请考虑以下每个代码格式设置建议。省略、扩展或修改这些示例规则以满足您团队的需求。
在所有情况下,请考虑您的团队将如何实施每个格式规则,然后让每个人都统一应用它。请返回参考您团队的风格以解决任何差异。您越少考虑格式设置,您就越能处理其他事情。
让我们看一下格式指南。
属性
属性提供了一种灵活的机制来读取、写入或计算类值。属性的行为就像它们是公共成员变量一样,但实际上它们是称为访问器的特殊方法。每个属性都有一个 get 和 set 方法,用于访问私有字段,称为支持字段。
通过这种方式,该属性封装了数据,使其不被用户或外部对象进行不必要的更改。getter 和 setter 都有自己的访问修饰符,允许您的属性是读写、只读或只写的。
您还可以使用访问器来验证或转换数据(例如,验证数据是否符合您的首选格式或将值更改为特定单位)。
属性的语法可能会有所不同,因此风格指南应定义如何设置属性的格式。使用以下提示来保持代码中的属性一致:
- 将 expression-bodied 属性用于单行只读属性 (=>):这将返回私有支持字段。
// EXAMPLE: expression bodied properties
public class PlayerHealth
{
// the private backing field
private int maxHealth;
// read-only, returns backing field
public int MaxHealth => maxHealth;
// equivalent to:
// public int MaxHealth { get; private set; }
}
其他所有内容都使用较旧的 { get; set; } 语法:如果您只想公开公共属性而不指定支持字段,请使用自动实现属性。对 set 和 get 访问器应用 expression-bodied 语法。如果您不想授予 write 访问权限,请记住将 setter 设为 private。将结束语与多行代码块的左大括号对齐。
// EXAMPLE: expression bodied properties
public class PlayerHealth
{
// backing field
private int _maxHealth;
// explicitly implementing getter and setter
public int MaxHealth
{
get => _maxHealth;
set => _maxHealth = value;
}
// write-only (not using backing field)
public int Health { private get; set; }
// write-only, without an explicit setter
public SetMaxHealth(int newMaxValue) => _maxHealth = newMaxValue;
}
序列化
脚本序列化是将数据结构或对象状态转换为 Unity 以后可以存储和重建的格式的自动过程。出于性能原因,Unity 处理序列化的方式与其他编程环境中不同。
序列化的字段显示在 Inspector 中,但您无法序列化静态、常量或只读字段。它们必须是公共的或使用 [SerializeField] 属性进行标记。Unity 仅序列化某些字段类型,因此请参阅[文档页面](Unity - Manual: Script serialization (unity3d.com))了解完整的序列化规则集。
使用序列化字段时,请遵循一些基本准则:
- 使用 [SerializeField] 属性:SerializeField 属性可以使用私有或受保护的变量,使其显示在检查器中。这比将变量标记为 public 更好地封装数据,并防止外部对象覆盖其值。
- 使用 Range 属性设置最小值和最大值:如果要限制用户可以分配给数字字段的内容,则 [Range(min, max)] 属性非常方便。它还方便地将字段表示为 Inspector 中的滑块。
- 将数据分组到可序列化的类或结构中以清理检查器:定义公共类或结构,并使用 [Serializable] 属性对其进行标记。为要在 Inspector 中公开的每种类型定义公共变量。
// EXAMPLE: a serializable class for PlayerStats
using System;
using UnityEngine;
public class Player : MonoBehaviour
{
[Serializable]
public struct PlayerStats
{
public int MovementSpeed;
public int HitPoints;
public bool HasHealthPotion;
}
// EXAMPLE: The private field is visible in the Inspector
[SerializeField]
private PlayerStats _stats;
}
从另一个类引用这个可序列化的类。生成的变量显示在 Inspector 的可折叠单元中。
大括号或缩进样式
C# 中有两种常见的缩进样式:
- Allman 样式将左大括号放在新行上,也称为 BSD 样式(来自 BSD Unix)。
- K&R 样式,或“一个真正的大括号样式”,使左大括号与前一个标题在同一行。
// EXAMPLE: Allman or BSD style puts opening brace on a new line.
void DisplayMouseCursor(bool showMouse)
{
if (!showMouse)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
else
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
}
// EXAMPLE: K&R style puts opening brace on the previous line.
void DisplayMouseCursor(bool showMouse){
if (!showMouse) {
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
else {
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
}
这些缩进样式也有变化。本指南中的示例使用 Microsoft Framework Design Guidelines 中的 Allman 样式。无论您选择哪一个团队,请确保每个人都遵循相同的缩进和大括号样式。
请尝试以下提示:
- 确定统一缩进:这通常是四个或两个空格。让团队中的每个人都就 Editor 首选项中的设置达成一致,而不会引发制表符与空格的激烈争论。请注意,Visual Studio 提供了将制表符转换为空格的选项。
在 Visual Studio (Windows) 中,导航到 C# >选项卡>>文本编辑器工具>选项”。
在 Visual Studio for Mac 上,导航到 C# 源代码> Preferences > 源代码。选择 文本样式 以调整设置。
- 尽可能不要省略大括号,即使是单行语句:这可以提高一致性,使代码更易于阅读和维护。在此示例中,大括号清楚地将操作 DoSomething 与循环分开。如果以后需要添加 Debug 行或运行 DoSomethingElse,则大括号已经就位。将子句保留在单独的行上,可以轻松添加断点。
// EXAMPLE: keep braces for clarity...
for (int i = 0; i < 100; i++) { DoSomething(i); }
// … and/or keep the clause on a separate line.
for (int i = 0; i < 100; i++)
{
DoSomething(i);
}
// AVOID: omitting braces
for (int i = 0; i < 100; i++) DoSomething(i);
- 不要从嵌套的多行语句中删除大括号:在这种情况下,删除大括号不会引发错误,但可能会造成混淆。为清楚起见,应用大括号,即使它们是可选的。
// EXAMPLE: keep braces for clarity
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
ExampleAction();
}
}
// AVOID: removing braces from nested multi-line statements
for (int i = 0; i < 10; i++)
for (int j = 0; j < 10; j++)
ExampleAction()
- 标准化你的 switch 语句:格式可能会有所不同,因此请在你的风格指南中记录你的团队偏好。下面是缩进 case 语句的一个示例。
// EXAMPLE: indent cases from the switch statement
switch (someExpression)
{
case 0:
DoSomething();
break;
case 1:
DoSomethingElse();
break;
case 2:
int n = 1;
DoAnotherThing(n);
break;
}
什么是 EditorConfig?
您是否有多个开发人员使用不同的编辑器和 IDE 处理同一个项目?请考虑使用 EditorConfig 文件。
EditorConfig 文件可以帮助您定义适用于整个团队的编码样式。许多 IDE(如 Visual Studio 和 Rider)都捆绑了原生支持,不需要单独的插件。
EditorConfig 文件易于阅读,并与版本控制系统配合使用。您可以在此处查看示例文件。EditorConfig 中的代码样式随您的代码一起移动,甚至可以在 Visual Studio 之外强制执行编码样式。
EditorConfig 设置优先于全局 Visual Studio 文本编辑器设置。当您在没有 .editorconfig 文件的代码库中工作时,或者当 .editorconfig 文件未覆盖特定设置时,您的个人编辑器首选项仍然适用。
有关一些实际示例,请参阅 real-world samples。
水平间距
像间距这样简单的操作可以增强代码在屏幕上的外观。您的个人格式首选项可能会有所不同,但请尝试以下建议来提高可读性:
- 添加空格以降低代码密度:额外的空格可以在行的各个部分之间产生视觉上的分隔感。
// EXAMPLE: add spaces to make lines easier to read
for (int i = 0; i < 100; i++) { DoSomething(i); }
// AVOID: no spaces
for(inti=0;i<100;i++){DoSomething(i);}
- 在函数参数之间的逗号后使用单个空格。
// EXAMPLE: single space after comma between arguments
CollectItem(myObject, 0, 1);
// AVOID:
CollectItem(myObject,0,1);
- 不要在括号和函数参数后添加空格。
// EXAMPLE: no space after the parenthesis and function arguments
DropPowerUp(myPrefab, 0, 1);
//AVOID:
DropPowerUp( myPrefab, 0, 1 );
- 不要在函数名称和括号之间使用空格。
// EXAMPLE: omit spaces between a function name and parenthesis.
DoSomething()
// AVOID
DoSomething ()
- 避免在括号内使用空格。
// EXAMPLE: omit spaces inside brackets
x = dataArray[index];
// AVOID
x = dataArray[ index ];
- 在流控制条件前使用单个空格:在流比较运算符和括号之间添加一个空格。
// EXAMPLE: space before condition; separate parentheses with a
space.
while (x == y)
// AVOID
while(x==y)
- 在比较运算符前后使用单个空格。
// EXAMPLE: space before condition; separate parentheses
with a space.
if (x == y)
// AVOID
if (x==y)
-
保持简短的线路。考虑水平空白:确定标准行宽(80-120 个字符)。将长行拆分为较小的语句,而不是让它溢出。
-
保持缩进/层次结构:缩进代码以提高可读性。
-
除非需要提高可读性,否则不要使用列对齐方式:这种类型的间距会对齐变量,但可能会使类型与名称配对变得困难。但是,列对齐对于具有大量数据的按位表达式或结构非常有用。请注意,当您添加更多项目时,它可能会给您带来更多的工作来保持列对齐。某些自动格式化程序还可能会更改列的对齐部分。
// EXAMPLE: One space between type and name
public float Speed = 12f;
public float Gravity = -10f;
public float JumpHeight = 2f;
public Transform GroundCheck;
public float GroundDistance = 0.4f;
public LayerMask GroundMask;
// AVOID: column alignment
public float Speed = 12f;
public float Gravity = -10f;
public float JumpHeight = 2f;
public Transform GroundCheck;
public float GroundDistance = 0.4f;
public LayerMask GroundMask;
垂直间距
您也可以利用垂直间距来发挥自己的优势。将脚本的相关部分放在一起,并使用空行来发挥自己的优势。请尝试以下建议,从上到下组织您的代码:
- 将依赖和/或类似的方法组合在一起:代码需要合乎逻辑且连贯。将执行相同操作的方法彼此相邻,这样读取 logic 的人就不必在文件中跳来跳去。
- 利用垂直空格来分隔类的不同部分:例如,您可以在以下两者之间添加两个空行:
- 变量声明和方法
- 类和接口
- if-then-else 块(如果它有助于可读性)
将此保持在最低限度,并在适用时在您的风格指南中注明。
region
region 指令使您能够折叠和隐藏 C# 文件中的代码部分,使大文件更易于管理和阅读。
但是,如果您遵循本指南中对 Classes 的一般建议,则您的 Class 大小应该是可控的,并且 #region 指令是多余的。将代码分成更小的类,而不是将代码块隐藏在区域后面。如果源文件较短,您将不太倾向于添加区域。
注意:许多开发人员将 regions 视为代码异味或反模式。作为一个团队决定你站在辩论的哪一边。
Visual Studio 中的代码格式设置
如果这些格式规则看起来势不可挡,请不要绝望。现代 IDE 可以高效地设置和执行它们。您可以创建格式规则模板,然后立即转换项目文件。
为脚本编辑器设置格式规则:
-
在 Visual Studio (Windows) 中,导航到 Tools (工具) > Options (选项)。找到 C# > Text Editor > Code Style Formatting。
使用这些设置可以修改 General、Indentation、New Lines、Spacing 和 Wrapping 选项。
-
在 Visual Studio for Mac 中,选择“Visual Studio”>“首选项”,然后导航到“源代码”>“代码格式> C# 源代码”。
如果您在任何时候都希望强制脚本文件符合样式指南:
- 在 Visual Studio (Windows) 中,转到编辑>高级 > 格式文档(Ctrl + K、Ctrl + D 热键和弦)。如果您只想设置空格和 Tab 对齐方式的格式,还可以使用编辑器底部的 Run Code Cleanup(Ctrl + K、Ctrl + E)。
- 在 Visual Studio for Mac 中,转到编辑>格式文档(Ctrl + I 热键)。
在 Windows 上,您还可以从 Tools > Import and Export Settings 共享编辑器设置。使用样式指南的 C# 代码格式导出文件,然后让每个团队成员导入该文件。
Visual Studio 使遵循样式指南变得容易。然后,格式化变得像使用热键一样简单。
注意:您可以配置 EditorConfig 文件(见上文),而不是导入和导出 Visual Studio 设置。这样做可以让您更轻松地在不同的 IDE 之间共享格式,并且它具有使用版本控制的额外好处。有关更多信息,请参阅 .NET 代码样式规则选项。
虽然这并不特定于干净的代码,但请务必查看使用 Visual Studio 在 Unity 中加快编程工作流程的 10 种方法。如果您应用这些工作效率技巧,则干净的代码更容易格式化和重构。
类
“在计算机简史中,没有人写过完美的软件。你不太可能是第一个。”- Andy Hunt 《The Pragmatic Programmer》作者。
根据 Robert C. Martin 的 Clean Code,类的第一条规则是它们应该很小。第二条规则是它们应该比这更小。
限制每个类的大小使其更加集中和有凝聚力。在现有类之上不断添加很容易,直到它的功能过度扩展。相反,有意识地努力保持课程简短。大而臃肿的类变得难以阅读和排除故障。
报纸的比喻
将类的源代码想象成一篇新闻文章。您从顶部开始阅读,标题和署名会吸引您的眼球。引言段落为您提供一个粗略的总结,然后随着您继续向下,您会收集更多详细信息。
记者称之为倒金字塔。大多数有新闻价值的文章的粗略笔触出现在开头。当你读到最后时,你只会得到故事的细微差别。
您的类也应遵循此基本模式。自上而下进行组织,并将您的函数视为形成层次结构。有些方法服务于更高层次,并为大局奠定基础。首先放置这些函数,然后放置具有实现详细信息的较低级别函数。
例如,您可以创建一个名为 ThrowBall 的方法,该方法引用其他方法 SetInitialVelocity 和 CalculateTrajectory。首先保留 ThrowBall,因为它描述了主操作。然后,在其下方添加支持的方法。
虽然每篇新闻文章都很短,但报纸或新闻网站会有很多这样的收集故事。当这些文章放在一起时,它们构成了一个统一的、功能性的整体。以同样的方式考虑您的 Unity 项目。它有许多类,这些类必须组合在一起才能形成一个更大但连贯的应用程序。
类组织
每个类都需要一些标准化。将类成员分组到各个部分以对其进行组织:
- 字段
- 属性
- 事件/委托
- 单行为方法(Awake、Start、OnEnable、OnDisable、OnDestroy 等)
- 公共方法
- 私有方法
回顾 Unity 中建议的类命名规则:源文件名必须与文件中 Monobehaviour 的名称匹配。文件中可能还有其他内部类,但每个文件只能存在一个 Monobehaviour。
单一职责原则
请记住,目标是使每个类都简短。在软件设计中,单一责任原则引导您走向简单。
这个想法是每个模块、类或函数负责一件事。假设您要构建一个 Pong 游戏。您可以从球拍、球和墙的类开始。
例如,Paddle 类可能需要:
- 存储有关球移动速度的基本数据
- 检查键盘输入
- 响应移动球拍
- 与球碰撞时播放声音
因为游戏设计很简单,所以你可以把所有这些东西合并到一个基本的 Paddle 类中。事实上,完全有可能创建一个 Monobehaviour 来做你需要的一切。
然而,将所有内容都放在一个类的一部分,即使是一个小类,也会因为混合职责而使设计复杂化。数据与输入交织在一起,而类需要对两者都应用逻辑。与 KISS 原则相反,你拿了一些简单的东西并将它们纠缠在一起。
相反,将您的 Paddle 类分成更小的类,每个类都有一项职责。将数据分离到其自己的 PaddleData 类中或使用 ScriptableObject。然后将其他所有内容重构为 PaddleInput 类、PaddleMovement 类和 PaddleAudio 类。
PaddleLogic 类可以处理来自 PaddleInput 的输入。应用来自 PaddleData 的速度信息,它可以使用 PaddleMovement 移动球拍。最后,当球与球拍碰撞时,PaddleLogic 可以通知 PaddleAudio 播放声音。
在这次重新设计中,每个类都做一件事,并适合小的、易于消化的部分。您无需滚动多个屏幕即可遵循代码。
你仍然需要一个 Paddle 脚本,但它的唯一工作是将这些其他类捆绑在一起。大部分功能被拆分为其他类。
请注意,干净的代码并不总是最紧凑的代码。即使使用较短的类,在重构期间,总行数也可能会增加。但是,每个单独的类都变得更容易阅读。当需要调试或添加新功能时,这种简化的结构有助于将所有内容保留在原处。
重构示例
要更深入地了解如何重构简单项目,请参阅如何在项目扩展时构建代码。本文演示了如何使用单一职责原则将较大的 Monobehaviour 分解成更小的部分。
你还可以在Unite Berlin观看Mikael Kalms的原始演讲,“From Pong to 15-person project”。
方法
“当你在阅读代码时,如果每个例程(函数或方法)都符合你的预期,那么你知道你在处理的是干净的代码。”-Ward Cunningham,Wiki 的发明者和极限编程的共同创始人之一。
像类一样,方法应该小巧且具有单一责任。每个方法应该描述一个动作或回答一个问题。它不应该同时做两件事。
一个方法的好名字反映了它的作用。例如,GetDistanceToTarget 是一个明确其预期目的的名字。
尝试以下建议,为你的自定义类创建方法:
- 使用更少的参数:参数会增加你方法的复杂性。减少它们的数量,使你的方法更容易阅读和测试。
- 避免过度重载:你可以生成无尽的方法重载排列。选择反映你将如何调用方法的几个,并实现这些。如果你确实重载了一个方法,通过确保每个方法签名具有不同数量的参数来防止混淆。
- 避免副作用:一个方法只需要做它的名字所宣传的事情。避免修改其作用域之外的任何内容。尽可能按值传递参数,而不是按引用。如果通过out或ref关键字返回结果,请确保那是你打算完成的事情。尽管副作用对某些任务很有用,但它们可能导致意外后果。编写一个没有副作用的方法,以减少意外行为。
- 与其传递一个标志,不如创建另一个方法:不要根据标志设置你的方法以在两种不同模式下工作。创建两个具有不同名称的方法。例如,不要创建一个GetAngle方法,根据标志设置返回度或弧度。相反,创建GetAngleInDegrees和GetAngleInRadians方法。
虽然布尔标志作为参数似乎无害,但它可能导致纠缠不清的实现或破坏单一责任。
扩展方法
扩展方法提供了一种向可能被密封的类添加其他功能的方法,并且可能是扩展 UnityEngine API 的简便方法。
要创建一个扩展方法,请使一个静态方法,并使用this关键字作为第一个参数之前,这将是你想扩展的类型。
例如,假设你想让一个名为 ResetTransformation 的方法来移除 GameObject 的任何缩放、旋转或平移。
你可以创建一个静态方法,传入 Transform 作为第一个参数,并使用 this 关键字:
/ EXAMPLE: Define an extension method
public static class TransformExtensions
{
public static void ResetTransformation(this Transform transform)
{
transform.position = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = Vector3.one;
}
}
然后,当您想要使用它时,请调用 ResetTransformation 方法。ResetOnStart 类在 Start 期间对当前 Transform 调用它。
// EXAMPLE: Calling the extension method
public class ResetOnStart : MonoBehaviour
{
void Start()
{
transform.ResetTransformation();
}
}
出于组织目的,请在 static 类中定义扩展方法。例如,为扩展 Transform 的方法创建一个名为 TransformExtensions 的类,为扩展 Vector3 的方法创建一个名为 Vector3Extensions 的类,等等。
扩展方法可以构建许多有用的实用程序,而无需创建更多的 Monobehaviours。参见 Unity Learn:扩展方法,将它们添加到你的游戏开发工具包中。
DRY原则:不要重复自己
在《程序员的实用指南》中,Andy Hunt和Dave Thomas 制定了DRY原则,即“不要重复自己”。这个在软件工程中经常提到的口号建议程序员避免重复或重复的逻辑。
这样做,你可以减轻修复错误和维护的成本。如果你遵循单一职责原则,你就不需要在修改一个类或方法时改变不相关的代码。在遵循DRY(Don't Repeat Yourself,不重复自己)原则的程序中,修复一个逻辑错误可以一劳永逸地解决问题。
DRY 的对立面是 WET(“我们喜欢打字”或“两次写一切”)。当代码中出现不必要的重复时,编程就是WET。
想象有两个 ParticleSystem(explosionA和explosionB)和两个 AudioClip(soundA 和 soundB)。每个 ParticleSystem 都需要与其各自的声音一起播放,你可以通过这样的简单方法来实现。
// EXAMPLE: WRITE EVERYTHING TWICE
private void PlayExplosionA(Vector3 hitPosition)
{
explosionA.transform.position = hitPosition;
explosionA.Stop();
explosionA.Play();
AudioSource.PlayClipAtPoint(soundA, hitPosition);
}
private void PlayExplosionB(Vector3 hitPosition)
{
explosionB.transform.position = hitPosition;
explosionB.Stop();
explosionB.Play();
AudioSource.PlayClipAtPoint(soundB, hitPosition);
}
这里每个方法都接受一个Vector3位置,将ParticleSystem移动到播放位置。首先,停止粒子(以防它们已经在播放),然后播放模拟。AudioSource 的静态 PlayClipAtPoint 方法然后在相同位置创建一个声音效果。
一个方法是另一个的剪切和粘贴版本,只需替换一些文本。虽然这可以工作,但每次你想创建一个爆炸时,你需要创建一个新方法——带有重复逻辑。
相反,将其重构为一个 PlayFXWithSound 方法,如下所示:
private void PlayFXWithSound(ParticleSystem particle, AudioClip clip, Vector3 hitPosition)
{
particle.transform.position = hitPosition;
particle.Stop();
particle.Play();
AudioSource.PlayClipAtPoint(clip, hitPosition);
}
添加更多 ParticleSystem 和 AudioClip,你可以继续使用这个方法来一起播放它们。
请注意,你可以在不违反 DRY 原则的情况下重复代码。更重要的是,你不要重复逻辑。
在这里,我们已经将核心功能提取到 PlayFXWithSound 方法中。如果你需要调整逻辑,你只需要在一个地方更改它,而不是在 PlayExplosionA 和PlayExplosionB 两个方法中。
注释
“到好处的注释可以增强代码的可读性。过多或轻率的注释可能会产生相反的效果。像所有事情一样,使用它们时要找到平衡。”-Cory House,软件架构师和作家
如果你遵循 KISS 原则并将代码分解为易于消化的逻辑部分,你的大部分代码不需要注释。良好命名的变量和函数将自我解释。
有用的注释不是回答“什么”,而是填补空白并告诉你“为什么”。你是否做出了不是立即明显的特定决策?是否有需要澄清的棘手逻辑?有用的注释揭示了代码本身无法获得的信息。
以下是一些关于注释的注意事项:
- 不要用注释来替代糟糕的代码:如果你需要添加注释来解释复杂的逻辑纠缠,重构你的代码使其更加明显。然后你就不需要注释了。
- 一个恰当命名的类、变量或方法本身就可以代替注释:代码是否自解释?如果是,那么减少冗余,跳过注释。
// AVOID: noisy, redundant comments
// the target to shoot
Transform targetToShoot;
- 尽可能将注释放在单独的一行,而不是代码行的末尾:在大多数情况下,为了清晰起见,让每个注释都单独一行。
- 使用双斜杠(//)注释标签:将注释保持在它解释的代码附近,而不是在开始处使用大型多行注释。保持它靠近有助于读者将解释与逻辑联系起来。
- 对于序列化字段,使用工具提示而不是注释:如果你在检视器中的字段需要解释,请添加一个工具提示属性,跳过单独的注释。工具提示将发挥双重作用。
// EXAMPLE: Tooltip replaces comment
[Tooltip(“The amount of side-to-side friction.”)]
public float Grip;
- 你还可以在公共方法或函数前使用摘要 XML 标签:Visual Studio 可以为许多常见的 XML 风格的注释提供 IntelliSense 支持。
// EXAMPLES:
// This is a common comment.
// Use them to show intent, logical flow, and approach.
// You can also use a summary XML tag.
//
/// <summary>
/// Fire the weapon
/// </summary>
public void Fire()
{
...
}
-
在注释分隔符 (//) 和注释文本之间插入一个空格。
-
添加法律免责声明:注释适用于许可证或版权信息。但是,请避免在代码中插入整个法律摘要。而是链接到包含完整法律信息的外部页面。
-
格式化你的注释:保持注释的统一外观,例如,每条注释以大写字母开头,以句点结尾。无论您的团队决定什么,请将其作为风格指南的一部分并遵循它。
-
不要在注释周围创建格式化的星号块或特殊字符:这会降低可读性,并导致代码混乱。
-
删除注释掉的代码:虽然在测试和开发期间注释掉语句可能很正常,但不要留下注释掉的代码。依赖你的源代码控制。然后勇敢地删除那两行代码。
-
保持你的 TODO 注释是最新的:当你完成任务时,确保清除你留下的 TODO 注释,这些注释是作为提醒的。过时的注释会分散注意力。
你可以在 TODO 中添加姓名和日期,以增加责任感和上下文。
同时,要现实一些。你在代码中五年前留下的那个 TODO?你可能永远不会去做它。记住 YAGNI(You Aren't Gonna Need It,你不会需要它)。除非你需要实现它,否则删除 TODO 注释。
-
避免日记:注释不是你的开发日记的地方。当你开始一个新类时,没有必要在注释中记录你正在做的每件事。适当的源代码控制使这变得多余。
-
避免归属:你不需要添加署名,例如// 由 devA 或 devB 添加,特别是如果你使用源代码控制。
常见陷阱
“干净代码不是偶然发生的。它是试图像团队一样思考和编码的个体的有意工作。”-Edsger W. Dijkstra,计算机科学先驱
当然,不是一切都按计划进行。不干净的代码不可避免地会发生,无论你多么努力尝试。你需要对它保持警惕。
代码异味是你项目中可能潜伏着问题代码的明显迹象。虽然以下症状并不一定指向潜在问题,但当它们出现时值得调查:
- 神秘的命名:每个人都喜欢一个好的谜团,除了他们的编码标准。类、方法和变量需要简单明了、无意义的名称
- 不必要的复杂性:当您试图预测对类的所有可能需求时,就会发生过度工程化。这可以表现为具有长方法或尝试执行太多操作的大型类的 God 对象。将一个大类分解为较小的专用部分,每个部分都有自己的职责。
- 缺乏灵活性:一个小的变更不应该要求你在其他地方进行多个变更。如果你遇到了这种情况,请再次检查你是否违反了单一职责原则。当你给某物赋予了多个职责时,它更容易出现问题,因为很难预见到一切。如果你更新了一个只做一件事的方法,更新后的逻辑仍然有效,你会期望你的代码的其他部分在之后继续正常工作。
- 脆弱性:如果你进行了一个小的更改,一切都停止了工作,这通常表明有问题。
- 不可移动性:你经常会编写在不同上下文中可重用的代码。如果它需要许多依赖项才能部署到其他地方,那么解耦它的工作方式。
- 重复代码:如果你注意到你已经剪切并粘贴了代码,那么是时候重构了。将核心逻辑提取到它自己的函数中,并从其他函数调用它。复制和粘贴的代码很难维护,因为每次更改逻辑时,你都需要在多个位置更新逻辑。
- 过多的注释:注释可以帮助解释代码不直观的部分。然而,开发人员可能会过度使用它们。对每个变量或语句的连续评论是不必要的。记住,最好的注释是良好命名的方法或类。如果你将逻辑分解为更小的部分,较短的代码片段需要较少的解释。
结论
“编程不是零和游戏。教给一个同行程序员一些东西并不会从你这里拿走。”-John Carmack,id Software 联合创始人
我们希望你享受了这次对干净编码原则的温和介绍。
这里介绍的技术与其说是一套特定的规则,不如说是一套习惯,像所有习惯一样,你需要通过日常应用自己发现它们。正如本指南前面提到的,你可以随意复制这个C#风格表,作为你自己指南的起点。 通过将代码分解为小的、模块化的片段来准备你的代码以实现可扩展性。随着开发的马拉松展开,预计你会一遍又一遍地重写你的代码。生产可能是一个具有变化需求的艰难过程。幸运的是,你不必独自面对。
当你作为团队编码时,游戏开发变得不像是漫长的单人赛跑,更像是接力赛。你有队友与你分担工作量,并且可以分割整个项目。
记得留在你的车道上,传递接力棒,你们将一起完成终点线。
如果你在寻找如何清理你的代码的帮助,请联系Unity的专业服务团队,Accelerate Solutions。该团队由Unity最资深的软件开发人员组成。Accelerate Solutions 提供定制的咨询和开发解决方案,专为各种规模的游戏工作室提供性能优化、开发加速、游戏规划、创新等服务。
Accelerate Solutions提供的一项服务是CAP(代码、资产和性能)。这个为期两周的咨询参与从对你的代码和资产进行为期三天的深入研究开始,以发现性能问题的根源。这将附带一个可操作的详细报告和最佳实践建议。要了解有关此或Unity Accelerate Solutions提供的其他服务的更多信息,请今天与Unity代表交谈。
参考文献
本指南是计算中使用的一组最佳实践的简短列表。有关更多信息,请参阅Microsoft框架设计指南,该指南作为本文档的总体风格指南。
你还可以从已经撰写的关于干净代码的全面卷中了解更多。以下是一些我们推荐你考虑进一步理解的书籍:
《代码大全:敏捷软件工艺手册》。Robert C. Martin,2008年。Prentice Hall。ISBN 978-0132350884。
《程序员的实用指南》20周年纪念版。David Thomas和Andrew Hunt,2019年,Addison Wesley,ISBN 978-0135957059。
附录:脚本模板
“与其花时间讨论和争论,不如通过编写代码来实际解决问题。”-Linus Torvalds,Linux 和 Git 的创建者
一旦你为你的风格指南建立了格式化规则,就配置你的脚本模板。这些模板为你在项目窗口中的创建菜单下创建脚本资产(如C#脚本、着色器或材料)生成空白起始文件。
在以下位置找到Unity的预配置脚本模板:
Windows:C:\Program Files\Unity\Editor\Data\Resources\ScriptTemplates
Mac:/Applications/Unity/Unity.app/Contents/Resources/ScriptTemplates
在 macOS 上,显示 Unity.app 包内容以显示Resources子目录。】
在这个路径下,你将看到默认模板。
81-C# Script-NewBehaviourScript.cs.txt
82-Javascript-NewBehaviourScript.js.txt
83-ShaderStandard Surface Shader-NewSurfaceShader.shader.txt
84-ShaderUnlit Shader-NewUnlitShader.shader.txt
无论何时你在项目窗口的创建菜单中创建新的脚本资产,Unity都会使用这些模板之一。
如果你用文本编辑器打开名为81-C# Script-NewBehaviourScript.cs.txt的文件,你将看到以下内容:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class #SCRIPTNAME# : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
#NOTRIM#
}
// Update is called once per frame
void Update()
{
#NOTRIM#
}
}
请注意关键字:
- #SCRIPTNAME#:这是你为脚本指定的名称。如果你没有自定义名称,它将使用默认名称,例如NewBehaviourScript。
- #NOTRIM#:这保证了空白,确保大括号之间出现一行。
脚本模板是可定制的。例如,你可以添加一个命名空间或删除默认的 Update 方法。修改模板可以节省你每次创建这些脚本资产时的一些时间。
模板文件名遵循这种模式:
PriorityNumber–MenuPath–DefaultName.FileExtension.txt
破折号(-)字符将文件名的不同部分分隔开:
- PriorityNumber 是脚本在创建菜单中出现的顺序。数字越低,优先级越高。
- MenuPath 允许你自定义文件在创建菜单中的外观。你可以使用双下划线(__)创建类别。 例如,“CustomScript__Misc__ScriptableObject”在创建菜单中创建了子菜单项 ScriptableObject,位于 Create.>.CustomScript.>.Misc 菜单下。
- DefaultName 是如果你没有指定一个,默认给资产的名称。
- FileExtension 是附加到资产名称的文件扩展名。 还要注意,每个脚本模板都有一个 .txt 附加到FileExtension。 如果你想将脚本模板应用于特定的 Unity 项目,请将整个 ScriptTemplates 文件夹直接复制到项目的 Assets 下。 复制到 Unity 项目的 ScriptTemplates。
接下来,创建新的脚本模板或修改原始模板以符合你的偏好。如果你不打算更改它们,请从项目中删除任何脚本模板。
例如,你可以为 ScriptableObjects 创建一个空白脚本模板。在 ScriptTemplates 文件夹下创建一个名为: 80-ScriptableObject-NewScriptableObject.cs.txt 编辑文本以阅读:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = “#SCRIPTNAME#”, menuName = “#SCRIPTNAME#”)]
public class #SCRIPTNAME# : ScriptableObject
{
#NOTRIM#
}
这将创建一个空白的 ScriptableObject 脚本,其中包含 CreateAssetMenu 属性。
保存脚本模板后,重新启动 Editor。下次您应该会在 Create (创建) 菜单中看到一个额外的选项。
从创建菜单创建一个新的 ScriptableObject 脚本(以及相应的 ScriptableObject 资产)。
确保备份你喜欢的脚本模板和原始模板。如果你需要恢复Unity未能识别的修改过的模板,你将需要这些文件。
一旦你有了一套你喜欢的脚本模板,将你的 ScriptTemplates 文件夹复制到一个新项目中,并根据你的特定需求自定义它们。你还可以更改应用程序资源中的原始脚本模板,但要小心。这会影响使用该版本的Unity的所有项目。
有关自定义脚本模板的更多信息,请参见此支持文章。还可以查看附加项目中的一些额外脚本模板示例。
附录:测试和调试
“调试的过程,就像在一部犯罪电影中扮演侦探,但同时你也是那个罪犯。”- Filipe Fortes
动化测试是提高代码质量并减少花在错误修复上的时间的有效工具。测试驱动开发(TDD)是一种开发方法,你在开发软件的同时创建单元测试。事实上,你通常会在特定功能功能之前编写每个测试用例。
当你开发软件时,你会反复运行它针对整个自动化测试套件的过程。这与先编写软件然后稍后构建测试用例形成鲜明对比。在 TDD 中,编码、测试和重构是交织在一起的。 这里有一个基本的想法,由Kent Beck的《测试驱动开发示例》提出:
- 添加一个单元测试:这描述了你想要添加到应用程序中的一个新功能;从你的团队或用户基础中规范需要完成的事情。
- 运行测试:测试应该失败,因为你还没有将新功能实现到你的程序中。此外,这验证了测试本身是否有效。它不应该默认始终通过。
- 编写最简单的代码通过新测试:编写足够的逻辑以使它通过新的单元测试。这不必是干净代码。它可以使用不优雅的结构、硬编码的魔术数字等,只要它通过单元测试。
- 确认所有测试通过:运行完整的自动化测试套件。你之前的单元测试应该全部通过。新代码满足你的新测试要求和旧要求。如果不是,修改你的新代码——只修改你的新代码——直到所有测试通过。
- 重构:回去清理你的新代码。使用你的风格指南确保一切符合。移动代码,使其逻辑组织。保持类似的类和方法在一起,等等。删除重复的代码,并重命名任何标识符以最小化注释的需要。拆分太长的方法或类。每次重构后运行自动化测试套件。
- 重复:每次添加新功能时都经历这个过程。每一步都是一个小的、增量的变化。在源控制下频繁提交。当调试时,你只需要检查每个单元测试的一小部分新代码。这简化了你的工作范围。如果一切都失败了,回滚到上一个提交,然后重新开始。
这就是它的要点。如果你使用这种方法开发软件,你往往会出于必要遵循KISS原则。一次添加一个功能,边测试边进行。每次测试后不断重构,使清洁代码成为一种持续的仪式。
像大多数干净代码的原则一样,TDD在短期内需要额外的工作,但通常会导致长期维护和可读性的改进。
Unity测试框架
Unity.Test.Framework(UTF),以前称为 Unity Test Runner,为 Unity 开发者提供了一个标准测试框架。UTF 使用 NUnit,这是一个用于 .NET 语言的开源测试库。
Unity 测试框架可以在编辑器(使用编辑模式或播放模式)和目标平台(例如,独立、Android、iOS)上执行单元测试。通过包管理器安装 UTF。在线文档将帮助你开始。
Unity测试框架的一般工作流程是:
- 创建一个新的测试套件,称为测试程序集:测试运行器UI简化了这个过程,并在项目中创建一个文件夹。
- 创建一个测试:测试运行器 UI 帮助你管理你将创建的 C# 脚本,作为单元测试。选择一个测试程序集文件夹,然后导航到 Assets.>.Create.>.Testing.>.C#.Test.Script。编辑这个脚本并添加测试逻辑。
- 运行测试:使用测试运行器 UI 运行所有单元测试或运行选定的一个。使用 JetBrains Rider,你还可以直接从脚本编辑器运行 UTF。
- 在编辑器或独立中添加播放模式测试:默认的测试程序集在编辑模式下工作。如果你想让单元测试在运行时工作,创建一个单独的播放模式程序集。为此配置你的独立构建(在编辑器中显示测试结果)。
有关如何开始使用 UTF 的更多信息,请参见测试框架微网站。 测试框架在编辑器中显示独立构建的结果。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库