当前位置: 首页 > 文档资料 > CatLib 中文文档 >

风格指南

优质
小牛编辑
133浏览
2023-12-01

这是CatLib特有的代码风格指南,如果您在您的项目中使用CatLib,为了避免错误,降低沟通成本,小纠结和 反模式,阅读本指南是一份不错的选择。

我们不能保证风格指南中的所有内容,对于所有工程和团队都是理想的,所以根据项目环境,周围技术环境,风格出现偏差是可行的。

我们应该尽可能的遵守本风格指南提出的建议。

根据周围技术堆栈对于命名规范相关我们建议您阅读微软提供的:框架设计指南

优先级定义

(A)必须

这些规范会帮你规避错误,您必须准守这些规范。这里可以存在例外,但是应该非常少见,只有您非常熟悉c#和周围技术栈且具备充足理由的情况下可以进行例外。

(B)强烈建议

这些规范能够在绝大多数项目中改善可读性和程序优雅度。即使你违反了,代码还是能照常运行,可以存在例外,但例外应该尽可能少且有合理的理由。

(C)建议

在这些规则里,我们提出一个默认的建议,如果理由充分,你可以随意在你的代码库中做出不同的选择。

(D)不建议

这些规则会导致代码变得难以维护甚至出现bug。这些规则列出了存在的潜在技术风险,并说明了它们什么时候不应该被使用。

(A)项目名始终作为命名空间的开头

命名空间的第一个片段为项目的名字。这样做可以避免不同的第三方服务提供者提供的类发生冲突。

错误的例子

namespace FileSystem
{
    public class FileSystem
    {
    }
}

正确的例子

namespace CatLib.FileSystem
{
    public class FileSystem
    {
    }
}

(A)服务的命名空间必须和服务的父级文件夹名一致

所有的服务(由多个类组成)命名空间必须和其父文件夹名字保持一致(对于根文件夹可以不包含在命名空间中,如:Providers),以避免通过目录检索时无法明确服务位置,同时可以避免现在以及未来的类名相冲突。

错误的例子

  • Providers/CatLib.FileSystem/AdapterLocal.cs
namespace CatLib.IO
{
    public class AdapterLocal
    {
    }
}
  • Providers/CatLib.FileSystem/OSS/AdapterAliyunOSS.cs
namespace CatLib.IO.OSS
{
    public class AdapterAliyunOSS
    {
    }
}

正确的例子

  • Providers/CatLib.FileSystem/AdapterLocal.cs
namespace CatLib.FileSystem
{
    public class AdapterLocal
    {
    }
}
  • Providers/CatLib.FileSystem/OSS/AdapterAliyunOSS.cs
namespace CatLib.FileSystem.OSS
{
    public class AdapterAliyunOSS
    {
    }
}

(A)文件名必须与类名一致

所有的类名必须和文件名一致,以避免通过目录来检索类时出现不一致的问题(这意味着我们不能将两个类写在同一个文件中,除非它是内部类)。

错误的例子

  • Providers/CatLib.FileSystem/AdapterLocal.cs
public class Local
{
}

正确的例子

  • Providers/CatLib.FileSystem/AdapterLocal.cs
public class AdapterLocal
{
}

(A)内部类的访问级别不能高于protected

所有的内部类,不允许将访问级别设定为publicinternal,因为如果内部类可以被外部访问,将会出现不可预测的问题。

错误的例子

public class FileSystem
{
    public class Disk
    {
    }
}

正确的例子

public class FileSystem
{
    private class Disk
    {
    }
}

(A)对待编译器警告视同错误

所有的编译器警告都应该被处理,忽略编译器警告可能会导致一些隐藏的bug。

错误的例子

int a = 0;
if(a == null) // 引发一个编译器警告
{
}

正确的例子

int a = 0;
if(a == 0)
{
}

示例中会导致完全不同的两种结果。

(A)对外服务的接口放在API命名空间下

对外服务的接口放在API命名空间下,这样在IED using的时候可以避免错误的使用实现代码而发生耦合。

一般来说我们放置在:项目名.API.组件名的命名空间下

错误的例子

  • Providers/CatLib.FileSystem/API/IFileSystem.cs
namespace CatLib.FileSystem
{
    public interface IFileSystem
    {
    }
}

正确的例子

  • Providers/API/CatLib.FileSystem/IFileSystem.cs
namespace CatLib.API.FileSystem
{
    public interface IFileSystem
    {
    }
}

(B)完整单词的类名

类名应该倾向于完整单词,而不是单词的缩写。

代码编辑器的自动补全功能已经让书写长类名的成本变得非常的低,而其带来的的明确性是非常宝贵的,我们不应该使用缩写来代替长类名。

错误的例子

Providers/
  CatLib.FileSystem/
    ManagerFS.cs
    OptsFS.cs

正确的例子

Providers/
  CatLib.FileSystem/
    ManagerFileSystem.cs
    OptionsFileSystem.cs

(B)紧密耦合的类名

如果一个类只在另外一个类的场景下有意义,这层关系应该体现在类名(包括文件名)上。因为编辑器通常会按字母顺序组织文件,所以这样做可以把相关联的文件排在一起。

错误的例子

Providers/
  CatLib.FileSystem/
    Handler.cs
    DirectoryHandler.cs
    FileForHandler.cs

正确的例子

Providers/
  CatLib.FileSystem/
    Handler.cs
    HandlerDirectory.cs
    HandlerFile.cs

(B)类名的单词顺序

类名应该以高级别的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾。

为什么不遵循自然语意

在自然的英文里,形容词和其它描述语通常都出现在名词之前,否则需要使用连接词。

  • coffee with sugar

如果你愿意,你完全可以在类名里包含这些连接词,但是单词的顺序很重要。

在你的类名中,所谓的高级别一般和语境有关,比如对于一个登录界面来说它可能包含下面这些类:

错误的例子

Providers/
  CatLib.LoginUI/
    AgreementCheckbox.cs
    LoginButton.cs
    PasswordInput.cs
    RegisterButton.cs
    TextInput.cs

我们可以发现我们很难发现那些类是针对输入这个功能的。现在我们根据单词顺序进行重命名:

正确的例子

Providers/
  CatLib.LoginUI/
    ButtonLogin.cs
    ButtonRegister.cs
    CheckboxAgreement.cs
    InputText.cs
    InputPassword.cs

因为编辑器通常会按字母顺序组织文件,所以将高级别的单词排在前类之间的重要关系一目了然。

(B)多级目录与类命名

(B)函数名和类名大小写

在声明函数和类的时候,其命名始终使用CamelCase。

我们遵循了微软提供的框架设计指南来确保 API 的一致性和易用性。

错误的例子

public class fileSystem
{
}

正确的例子

public class FileSystem
{
}

(B)将接口绑定到服务,而不是为服务设定别名

我们强烈建议将接口绑定到服务,而不是为服务设定别名。如果以别名的形式设定,很多事件将会无法使用,如:Watch。

错误的例子

App.Bind<FileSystem>().Alias<IFileSystem>();

错误点:直接使用了别名,而没有对实现进行绑定主要接口。

正确的例子

App.Bind<IFileSystem>(()=> new FileSystem());
App.Bind<IFileSystem, FileSystem>();
App.Bind<IFileSystem, FileSystem>().Alias<IDisk>();

如果存在多个接口需要指向一个服务,请使用别名功能。

(B)服务内的命名规范一致

对于一个服务中的命名规范强烈建议一致,例如:要么变量是_开头要么始终是m_开头(或者其他统一的规范,如:无标示符开头)。而不能交叉混用。

交叉混用会导致团队一致性下降,从而提高理解成本。单个组件一般由2-3人协同开发。我们强烈建议以服务为最小单元统一代码命名规范。

错误的例子

  • Providers/CatLib.FileSystem/FileSystem.cs
public class FileSystem
{
    private string defaultDiskName;
    private IDictionary<string, IDisk> m_disks;
}
  • Providers/CatLib.FileSystem/Disk.cs
public class Disk
{
    private string _diskName;
}

正确的例子

  • Providers/CatLib.FileSystem/FileSystem.cs
public class FileSystem
{
    private string defaultDiskName;
    private IDictionary<string, IDisk> disks;
}
  • Providers/CatLib.FileSystem/Disk.cs
public class Disk
{
    private string diskName;
}

(C)使用上下文关系来处理不同实例相同接口的服务

我们建议使用上下文关系来处理不同实例相同接口的服务,而不是使用在具体实现构造函数中通过获取指定服务。如果在实际的实现中获取服务实例,将会和框架耦合,并产生公共耦合。

而将这些关系定义在服务提供者中将会避免这些问题。

错误的例子

public class GameVideo : IGameVideo
{
    public GameVideo(IFileSystem fileSystem)
    {
        var disk = fileSystem.Get("oss");
    }
}

正确的例子

public class GameVideo : IGameVideo
{
    public GameVideo(IDisk disk)
    {
    }
}
App.Singleton<IGameVideo, GameVideo>()
    .Needs<IDisk>()
    .Given(()=> App.Make<IFileSystem>().Get("oss"));

(C)总是在服务提供者中来注册服务

我们建议服务在服务提供者中进行绑定,而不是在Register以外的其他地方。

错误的例子

  • Register之外的其他地方
protected override void OnStartCompleted()
{
    App.Singleton<IFileSystem, FileSystem>();
}

正确的例子

public class ProviderFileSystem : ServiceProvider
{
    public override void Register()
    {
        App.Singleton<IFileSystem, FileSystem>();
    }
}

(C)字符串常量的值,可以映射到实际类型

我们建议字符串常量,可以映射到实际有效的类型或者该常量本身。这样在未来需要通过常量值来分析时可以快速定位具体的类。

错误的例子

public class ApplicationEvents 
{
    public const string Bootstrapping = "bootstrapping";
}

正确的例子

public class ApplicationEvents 
{
    public const string Bootstrapping = "ApplicationEvents.Bootstrapping";
}

(C)一个服务只提供一个服务提供者

一个服务只提供一个服务提供者。如果一个服务提供了多个服务提供者将会导致沟通成本的上升,使用者无法了解不同服务提供者之间的区别或不知道该如何使用。

我们建议使用变量控制的方式来处理这个问题。

错误的例子

  • Providers/CatLib.FileSystem/ProviderFileSystemClean.cs
public class ProviderFileSystemClean : ServiceProvider
{
    public override void Register()
    {
        App.Singleton<IFileSystem, FileSystem>();
    }
}
  • Providers/CatLib.FileSystem/ProviderFileSystem.cs
public class ProviderFileSystem : ServiceProvider
{
    public override void Register()
    {
        App.Singleton<IFileSystem, FileSystem>()
            .OnResolving((instance) =>
            {
                var fileSystem = (FileSystem)instance;
                fileSystem.Extend(()=> new Disk(new AdapterLocal()), "local");
                fileSystem.Extend(()=> new Disk(new AdapterHttp()), "http");
            });
    }
}

正确的例子

  • Providers/CatLib.FileSystem/ProviderFileSystem.cs
public class ProviderFileSystem : ServiceProvider
{
    public bool ExtendDefaultAdapter { get; set; } = false;
    public override void Register()
    {
        var binder = App.Singleton<IFileSystem, FileSystem>();
        if(!ExtendDefaultAdapter)
        {
            return;
        }
        binder.OnResolving((instance) =>
        {
            var fileSystem = (FileSystem)instance;
            fileSystem.Extend(()=> new Disk(new AdapterLocal()), "local");
            fileSystem.Extend(()=> new Disk(new AdapterHttp()), "http");
        });
    }
}

(C) 避免使用 protected 变量

关系密切的概念应该互相靠近,否则就会导致在某个类中进行摸索,一个函数跳到另外一个函数,上下求索,弄清这些函数如何操作,如何互相关系,或是了解变量与函数的继承链条,所以除非有很好的理由,否则就不要把关系密切的概念放到不同的文件之中,这也是我们不建议使用protected变量的原因,因为它会破坏这一关系。

错误的例子

public class Foo
{
    protected string foo;
}

正确的例子

public class Foo
{
    private string foo;
}

(C)可拆分单词的命名

有些时候我们可能纠结于可拆分单词如何进行命名,如:username可以被拆分为username。虽然这些单词可以被拆分但是我们需要注意username为英文中的一个整体单词。所以我们应该视作为一个单词。

错误的例子

public class LoginUI
{
    private string userName; // UserName, _userName 都是错误的例子
}

正确的例子

public class LoginUI
{
    private string username; // Username, _username 都是正确的例子
}

(C)门面应该放置在Facades命名空间下

属于门面的代码应该被放置在项目名.Facades的命名空间下。

错误的例子

  • Providers/CatLib.FileSystem/Facades/FileSystem.cs
namespace CatLib.FileSystem
{
    public class FileSystem : Facade<IFileSystem>
    {
    }
}

正确的例子

  • Providers/CatLib.FileSystem/Facades/FileSystem.cs
namespace CatLib.Facades
{
    public class FileSystem : Facade<IFileSystem>
    {
    }
}

门面作为一个特殊存在,所以我们允许其命名空间例外于其他规范。

(C)对外提供的接口总是放在API文件夹下

我们建议对外提供服务的接口放在Providers/API/组件名文件夹下,这样可以达成接口即文档的意义。

内部使用的接口可以直接放在组件实现的文件夹中,而无需放在API文件夹下。

错误的例子

- Providers/
- - FileSystem/
- - - API/
- - - - IFileSystem.cs
- - - - IDisk.cs
- - - FileSystem.cs
- - - ProviderFileSystem.cs
- - Debugger/
- - - API/
- - - - IDebugger.cs
- - - Debugger.cs
- - - ProviderDebugger.cs

正确的例子

- Providers/
- - API/
- - - FileSystem/
- - - - IFileSystem.cs
- - - - IDisk.cs
- - - Debugger/
- - - - IDebugger.cs
- - FileSystem/
- - - FileSystem.cs
- - - ProviderFileSystem.cs
- - Debugger/
- - - Debugger.cs
- - - ProviderDebugger.cs

(D)在循环中生成Lambda表达式,并尝试访问迭代器变量。

在循环中生成Lambda表达式,并尝试访问迭代器变量时,会导致迭代器变量不是预期值的问题。

错误的例子

foreach (var index in new int[1, 2, 3, 4, 5])
{
    closure(()=> index); // index 为预期外的值
}

正确的例子

foreach (var index in new int[1, 2, 3, 4, 5])
{
    var localIndex = index;
    closure(()=> localIndex);
}

迭代器会导致index发生变化,从而使lambda表达式不能返回正确的index值。

(D)不要让泛型方法支持虚函数重载

在一些平台下(如:unity3d)使用的静态编译(AOT)技术会导致泛型方法虚函数调用非常危险,会导致下面AOT裁剪异常:

Attempting to call method 'xxxxxxxxxxxx' for which no ahead of time (AOT) code was generated.

错误的例子

public virtual void GenericMethod<T1,T2>(T1 data) // 一旦虚函数被覆盖(override)并调用会引发AOT异常
{
}

正确的例子

public void GenericMethod<T1,T2>(T1 data)
{
}