面向C#新学者,介绍命名空间(namespace)的概念以及C#中的命名空间的相关内容
1. 阅读基础
理解C与C#语言的基础语法
2. 名称冲突与命名空间 2.1 一个生活例子
假设猫猫头在北京有一个叫AAA的朋友,在上海有两个叫AAA的朋友,上海的两个AAA一个喜欢咸粽子,一个喜欢甜粽子。有一天猫猫找朋友玩,朋友问道:
“AAA最近过得怎么样”,
然而猫猫头有三个叫AAA的朋友,因此猫猫头不确定朋友问的是哪个AAA,于是朋友改问:
“上海的那个AAA最近过得怎么样”
精确了一点,但这还不够,因为猫猫头在上海认识两个叫AAA的朋友,于是朋友再次改问:
“上海的那个喜欢咸粽子的AAA最近过得怎么样。
到这里,猫猫头就确定了朋友问的是哪个小明。也就是说,通过地域+喜好+姓名,猫猫头可以确定朋友指的具体的人。
这个例子体现的就是命名空间的实质:限定性修饰。
2.2 从C语言的缺陷到命名空间(1)函数命名冲突
在谈论什么是命名空间之前,我们先来看一看C语言中存在的一些问题。假设你和你的小伙伴同时开发一个C程序,并且你们很巧地定义了两个函数名相同的函数:
void Init() { }
void Init() { }
假设这两个函数做的事完全不同(一个用来初始化控制台,一个用来初始化打印机)而无法合并,那么显然此时需要用一个办法来区分两个函数。经过简单讨论,你和你的小伙伴决定在每个函数名前添加函数的作用对象名字加以区分,于是你们把函数名改成了如下:
void ConsoleInit() { } // 用于初始化控制台的Init
void PrinterInit() { } // 用于初始化打印机的Init
随着开发进度的推进,你们创建的同名函数可能会越来越多,最后函数名看起来很可能像下面这样:
void ConsoleInit() { }
void ConsoleFoo() { }
void ConsoleWhatever() { }
void ConsolePrint(const char* s) { }
...
void PrinterInit() { }
void PrinterFoo() { }
void PrinterWhatever() { }
void PrinterPrint(const char* s) { }
...
当然这样的函数名并不是不行,但是函数名中含有不必要的冗余信息,使用这种函数名会使代码可读性下降,更重要的是,这还会使得编写代码时所需要输入的字符量大大增加:
ConsoleInit();
ConsoleFoo();
ConsoleWhatever();
ConsolePrint("...");
在上述例子中,你需要使用的函数前都添加了Console前缀,哪怕此时其实你可以明确自己大部分时候都是在操作控制台,无论是使用还是阅读,这些前缀对你来说只是多余的。另一方面,假设有办法让编译器为某个范围内所有使用的函数名都自动添加‘Console’前缀,则可以像下面这样:
// 告诉编译器为下面的函数名都添加Console前缀
Init();
Foo();
Whatever();
Print("...");
显然此时使用函数就方便了许多。
(2)让编译器代劳
基于上述理由,可以定义一种语法来告诉编译器为接下来使用的函数名都添加指定前缀,例如:
using Console; // 告诉编译器为接下来所有的函数都添加Console前缀
Init();
Foo();
Whatever();
Print("...");
在这里,我们设定使用using关键字来告诉编译器为所有的函数都添加Console前缀,这样在编译器看来,上述实际代码就如下:
ConsoleInit();
ConsoleFoo();
ConsoleWhatever();
ConsolePrint("...");
显然此时程序依然可以准确地调用合适的函数。
(2)更进一步
既然可以让编译器在调用函数时自动为其添加前缀,那么为何不让编译器为也在我们定义函数时为函数名自动添加前缀?所以,还可以定义一种语法来告诉编译器为定义的函数的函数名自动添加前缀,在这里,我们假设使用namespace(很快你就知道为什么了)关键字指示需要添加的前缀,并让编译器在其后的代码块中定义的所有函数都添加所需的前缀,就像如下:
namespace Console // 在后面代码块中定义的函数前添加Console
{
void Init() { }
void Foo() { }
void Whatever() { }
void Print(const char* s) { }
}
现在我们规定上述语法相当于告诉编译器:编译时为所有在代码块中定义的函数自动添加Console前缀,所以在编译器进行自动转换后,上述的代码就会像下面这样:
void ConsoleInit() { }
void ConsoleFoo() { }
void ConsoleWhatever() { }
void ConsolePrint(const char* s) { }
使用这种语法,像下面这样定义函数不仅简化了函数名,同时可以避免了潜在的函数名冲突:
namespace Console // 告诉编译器为其后代码块中所有的定义的函数添加Console前缀
{
void Init() { }
// ...
}
namespace Printer // 告诉编译器为其后代码块中所有的定义的函数添加Printer前缀
{
void Init() { }
// ...
}
甚至更近一步,可以再允许使用嵌套语法添加前缀:
namespace MeAndFriend
{
namespace Console // 告诉编译器为代码块中所有的函数添加Console前缀
{
void Init() { }
// ...
}
namespace Printer // 告诉编译器为代码块中所有的函数添加Printer前缀
{
void Init() { }
// ...
}
}
例如这样上述代码就会由编译器生成为类似‘MeAndFriendConsoleInit’与‘MeAndFriendPrinterInit’这样的函数名。显然这种语法大幅减少了需要输入的内容,并且结合前面的using语法,调用时也方便:
using MeAndFriendConsole; // 告诉编译器接下来所有的函数默认都有MeAndFriendConsole前缀
Init();
... // 其他同样需要MeAndFriendConsole前缀的函数
2.3 命名空间
上面的例子中提到的那些所谓由编译器自动的‘前缀’,我们可以给它一个好听的名字:命名空间(namepsace),或者也可以称其为名称空间/名字空间。从上述例子可以看出,命名空间其实就是精准定位到具体成员所用的限定修饰。命名空间其实不是必须的东西,例如如果是为了避免函数名冲突,你完全可以通过为函数名添加各种限定词的来避开冲突,然而正如前面所看到的,如果每个函数在定义和调用的时候都要输入如此多的和函数所做的事无关的附加信息,那么输入和阅读代码都是额外的负担,并且可能会对以后可能的代码修改带来诸多不便,而命名空间的出现以及相关语法支持在很大程度上减缓了这一问题。
3. C#中的命名空间
命名空间是如此有用的东西,以至于许多现代化的编程语言都有类似命名空间的设计。C#自然也有一套自己的命名空间体系,MSDN上对命名空间的定义是‘包含一组相关对象的作用域’,这一概念有点抽象,接下来我们从具体的使用中来理解。
3.1 使用 3.1.1 声明命名空间:namespace关键字(1)基本命名空间
要在C#中声明一个命名空间,只需要使用namespace关键字并加上命名空间的名称与一对花括号(即代码块)即可:
namespace Alpha
{
}
在该命名空间的代码块中定义的类型都会作为属于命名空间的类型,例如:
namespace Alpha
{
class Foo
{
}
}
上述代码声明了一个命名空间Alpha,并在Alpha下定义了一个Foo对象。按照命名空间的实际意义来说,如果用句点.
来连接命名空间与类型名,那么Foo类型的完整名称就是Alpha.Program。利用命名空间,可以像下面这样定义相同的类型名而不会发生冲突:
namespace Alpha
{
class Foo
{
}
}
namespace Beta
{
class Foo
{
}
}
尽管上述代码中出现了两个名称为Foo的类,但两者的完整类型名分别为Alpha.Foo与Beta.Foo,在程序看来这是两个完全不同的类型。编译器在编译时会将类型名替换为完整的类型名(即命名空间+类型名的组合),因此程序运行时可以准确定位到具体类型而不会出现混乱。
为了方便后文的阐述,我们将这种‘以所属命名空间的完整名称.类型名
格式表达的类型名’称为‘完整类型名’。
(2)嵌套命名空间
可以嵌套声明命名空间:
namespace Alpha
{
namespace Beta
{
class Program
{
}
}
}
此时上述代码中的Program类型的完整类型名为Alpha.Beta.Program。不过嵌套命名空间会浪费大量的列缩进(按照格式规范,每一级代码块中的代码需要缩进4个空格,因此每多一层命名空间就会导致所有代码多缩进4个空格),因此还可以通过使用句点.来连接命名空间以表示命名空间的嵌套关系,对于上述命名空间的嵌套也可以采用下述声明方法:
namespace Alpha.Beta
{
class Program
{
}
}
接着从概念上来讲,Alpha是根空间,而Beta则是Alpha的子空间。此外,所有命名空间都有一个共同的根空间,被称为全局命名空间,它是隐式且匿名的,全局命名空间下的内容可以在不添加额外限定的情况下直接访问。例如,下面是一个位于全局命名空间的类:
class Foo
{
}
此后若需要使用此Foo类型则可以直接使用其类型名‘Foo’,而不需要添加额外的限定(前提是不与使用时所处的命名空间类型名冲突,如果冲突需要添加额外限定,后文会提到)。简单来说可以视Foo类型没有所属的命名空间,但从概念上来讲,类型Foo依然属于一个命名空间,只不过这个命名空间是隐式且匿名的。
3.1.2 使用命名空间:using关键字(1)using指令
同一个命名空间下的类型之间可以直接使用其类型名访问,例如对于以下类型定义:
namespace Alpha
{
class Foo { }
}
类型Foo的完整类型名是Alpha.Foo,但在Alpha命名空间内使用Foo类型时可以直接使用其类型名称‘Foo’:
namespace Alpha
{
class Foo { }
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo();
}
}
}
这一规则同样适用于其子空间:
namespace Alpha
{
class Foo { } // 定义为Alpha空间下的Foo类型
namespace Beta // Beta是Alpha的子空间
{
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo(); // 同样可以直接使用类名指示类型
}
}
}
}
另一方面,如果命名空间要使用其子空间中定义的类型,则可以通过子空间名.类型名访问,也就是相当于以本命名空间为起点使用目标类型,例如:
namespace Alpha
{
namespace Beta // Beta是Alpha的嵌套命名空间
{
class Cat { } // 定义在Alpha.Beta下的Cat类
}
class Program
{
static void Main(string[] args)
{
Beta.Cat cat = new Beta.Cat(); // 使用子空间名+类型名指定类型
}
}
}
然而,如果要在其他命名空间中使用Alpha命名空间下的Foo,则需要使用其完整类型名Alpha.Foo,例如在Test命名空间下使用Alpha.Foo:
namespace Test
{
class Program
{
static void Main(string[] args)
{
Alpha.Foo foo = new Alpha.Foo(); // 使用完整类型名
}
}
}
显然使用完整类型名是一件很繁琐的事,C#自然提供了相应的解决方法。如果要想在如同Alpha命名空间中一样简单地直接使用Foo,可以使用using指令:
using Alpha; // using指令,导入Alpha命名空间
namespace Beta
{
class Program
{
static void Main(string[] args)
{
Foo foo1 = GetFoo();
}
static Foo GetFoo() { ... }
static void CheckFoo(Foo foo) { ... }
}
}
在using关键字后面跟随命名空间名,可以将指定的命名空间‘导入’到本文件中。所谓‘导入’就是告诉编译器如果代码中如果出现了类型名,那么除了在本命名空间范围查找类型外,还可以在由using指令导入过的命名空间范围查找。当编译器查找到指定类型后会将类型名替换为其完整类型名。也就是说,上述的using Alpha指令告诉编译器可以在Alpha命名空间中查找类型,因此当编译器发现Main方法的Foo类型没有在Beta命名空间中定义时,会从Alpha命名空间中查找,在Alpha下发现Foo后,编译器将Main中的Foo替换为其完整类型名Alpha.Foo。因此上述代码在编译后等同于去掉using指令并将所有的Foo替换为Alpha.Foo的编译结果。
另外,可以同时使用多个using语句来导入多个命名空间,并且using的顺序不影响程序行为,不会出现类似‘后面using的命名空间覆盖前面using的命名空间中具有相同名称的类型’的问题。另外,同一个using指令可以重复声明,尽管从实际来说这一行为没有意义。因此,下述的using声明的作用都是一致的:
// 1. 先Alpha再Beta
using Alpha;
using Beta;
// 2. 先Beta再Alpha
using Beta;
using Alpha;
// 3. 重复使用相同的using指令,可行,但无意义,编译器会警告
using Alpha;
using Alpha;
using Beta;
using Beta;
using Beta;
从实际行为来说,using指令只是告诉编译器如果代码中如果出现了类型名,那么除了在本命名空间范围内查找对应类型外,还可以在由using指令导入过的命名空间范围内查找。using指令的作用域是文件范围,也就是说一个using指令对使用该using指令的整个文件都有效,基于这个原因,using指令被要求放置在文件开头以清楚描述其行为。
(2)using别名
你可以使用using为类型定义别名:
using Alias = Alpha.Foo;
Alias foo = new Alias(); // 等同于Alpha.Foo foo = new Alpha.Foo()
通过使用using <别名> = <完整类型名>
,可以为指定类型指定一个别名,在其后的代码中可以使用该别名来指代该类型,例如上述代码中为Alpha.Foo类型指定了别名Alias,编译器在遇到代码中出现使用Alias类型的地方就会将其替换为Alpha.Foo。另外,using别名也适用于泛型:
using CatList = System.Collections.Generic.List<Cat>;
CatList cats = new CatList();
using别名作用域也是整个文件,因此基于同样的原因,using别名的声明也要求放在文件开头。
3.1.3 global关键字默认情况下,编译器获取查找类型时会优先以当前命名空间为起点查找:
class Foo { } // 位于全局命名空间的Foo
namespace Alpha
{
class Foo { } // 位于命名空间Alpha的Foo
class Program // 位于命名空间Alpha的Program
{
static void Main(string[] args)
{
Foo foo = new Foo(); // 此时的Foo是Alpha.Foo,因为编译器优先从当前命名空间查找
}
}
}
上述代码中Main方法中的Foo是Alpha.Foo,原因是编译器首先以命名空间Alpha为起点查找‘Foo’时就发现了Foo的类型定义,因此不会再去查找全局命名空间中的Foo。此时如果要使用全局命名空间中的Foo,则需要告知编译器从全局命名空间开始查找(而非当前命名空间),可在通过在类型名前添加global::
达到此目的:
global::Foo foo = new global::Foo(); // 告诉编译器从全局命名空间开始查找,此时Foo就是位于全局命名空间的那个Foo
可以认为,global就是全局命名空间的‘名字’,只不过后面需要接::
而不是.
(写过C++的朋友可能会对这一语法感到颇为熟悉)。另外,可以结合global与using指令进行全局的命名空间导入,这在后文会提到。
(1)导入的命名空间与当前命名空间存在类型名冲突
有时候导入的命名空间中可能存在与当前命名空间中冲突的类型名,例如:
文件1内容:
namespace Alpha
{
class Foo { }
}
文件2内容:
using Alpha;
namespace Test
{
class Foo { }
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo(); // Alpha.Foo还是Test.Foo?
}
}
}
文件1中的Alpha命名空间中定义了一个Foo对象,文件2中使用using指令导入了Alpha命名空间,但同时在其命名空间Test下也定义了一个Foo,并且Main方法中使用的不是完整类型名,那么上述代码使用的应该是哪一个Foo?答案是Test.Foo,也就是本命名空间下的Foo。原因在前文提到过,就是编译器获取类型时会优先以当前命名空间为起点查找,只有当前命名空间下找不到才会从导入过的命名空间中查找。因此,此时如果要使用Alpha下的Foo,依然需要其使用完整类型名:
Alpha.Foo foo = new Alpha.Foo();
注意此时using别名无效,原因同样是编译器获取类型时会优先以当前命名空间为起点查找,这比using别名的优先级高。
(2)导入的命名空间之间存在类型名冲突
多个using指令导入的命名空间之间也可能出现类型名冲突,例如两个文件的文件内容如下:
文件1内容:
namespace Alpha
{
class Foo { }
class Cat { }
}
namespace Beta
{
class Foo { }
class Dog { }
}
文件2内容:
using Alpha;
using Beta;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Cat cat = new Cat(); // 是Alpha.Cat
Dog dog = new Dog(); // 是Beta.Dog
Foo foo = new Foo(); // Alpha.Foo还是Beta.Foo?
}
}
}
文件2中使用两个using指令分别导入了Alpha于Beta命名空间,并在Main方法中使用了这两个命名空间下的类型。其中Cat只在Alpha命名空间下定义过,因此可以确认其完整类型名,同理Dog也可以。然而由于Alpha和Beta同时定义了Foo类型,并且using的顺序不影响程序行为,因此此时编译器无法确认Foo到底应该使用Alpha还是Beta命名空间下的版本。要解决这类问题,同样需要使用完整类型名:
Alpha.Foo foo = new Alpha.Foo();
Beta.Foo foo = new Beta.Foo();
当然,此时也可以使用using别名来指定Foo所代表的类型:
using Foo = Alpha.Foo; // 将Foo作为Alpha.Foo的别名
using Foo = Beta.Foo; // 或者将Foo作为Beta.Foo的别名
3.3 特殊命名空间
3.3.1 static命名空间
static命名空间用于简化静态类的成员调用。例如有以下静态类:
namespace Hello
{
static class Speaker
{
public static void Say();
}
}
在另一个文件中使用此静态类:
using Hello;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Speaker.Say();
Speaker.Say();
Speaker.Say();
}
}
}
上述用法没有问题,但是静态类不需要实例化,并且静态类在很多时候只是起到对代码的组织作用。换句话说,静态类的类名有时候其实并不重要,可以省略。为此,C#提供了一种特殊的using指令让程序员在调用静态类成员时可以省略其类名:
using static Hello.Speaker; // using static + 静态类的完整类型名
namespace Test
{
class Program
{
static void Main(string[] args)
{
Say();
Say();
Say();
}
}
}
上述代码中使用了using static + 静态类的完整类型名
向当前文件导入静态类,告诉编译器接下来的代码中使用的方法或字段如果没在本类型中找到定义,则可以从导入过的静态类的成员中查找。因此,上面的Main方法调用Say方法时,编译器发现Program类型中没有定义过Say方法,于是尝试从使用using static导入过的Hello.Speaker静态类中查找名为Say的方法。
普通的命名空间声明作用范围是其后面的代码块,但你也可以声明作用于整个文件范围的命名空间,声明后所有在该文件下定义的类型都将纳入此命名空间:
namespace Alpha; // 将Alpha声明为文件作用域的命名空间
class Cat { }
class Dog { }
声明作用于文件范围的命名空间和作用域代码块的命名空间语法相似,其最大的优点在于其可以减少格式化代码样式时所需的列缩进量。需要说明的是,声明作用于文件范围的命名空间有如下限制:
- 只能声明一次文件范围的命名空间,这是显而易见的
- 不能再声明普通命名空间,也就是说,下述代码无效,Beta也不会视为Alpha的子空间:
namespace Alpha;
namespace Beta
{
}
3.3.3 全局命名空间
默认的using指令作用域是文件,也就是说一个using指令的声明只对使用了该using指令的这一个文件有效。但有时候一个命名空间可能会频繁用于多个文件,例如System命名空间相当常用,很多文件都需要额外添加using System来导入此命名空间,这有时候会为编码带来枯燥的体验,为此,C#提供了一种名为全局using的导入方法,按此using导入的命名空间会作用于整个项目,只需要在using指令前添加global关键字即可将命名空间其作为全局命名空间导入:
global using System;
在一个项目中的任意一个文件中使用以上using声明后,该项目中所有的文件都会默认导入过System命名空间。另外,语法规定全局using必须位于普通using之前,因此建议将全局using写入到单个文件中。
4. 命名空间杂谈 4.1 完整类型名的查找流程
编译器会按以下流顺序查找类型:
(上述流程中,如果在某一蓝色块找到了所需的类型就跳出,否则按箭头顺序进入下一个蓝色块查找)
不需要刻意记忆该流程,上述流程最主要的意义在于说明命名空间的一些行为。本身的意义不大,实际代码编写时根据IDE提示与运行结果可轻易解决相关问题。
4.2 命名空间的“命名”MSDN上给出了命名空间的命名建议:
<Company>.(<Product>|<Technology>)[.<Feature>][.<Subnamespace>]
- 在命名空间名称前加上公司名称或个人名称。
- 在命名空间名称的第二层使用稳定的、与版本无关的产品名称。
- 使用帕斯卡命名法,并使用句点分隔命名空间组件。不过,如果品牌名有自身的大小写规则,则遵循使用品牌名。
- 在适当的情况下使用复数命名空间名称,首字母缩写单词例外。
- 命名空间不应该与其作用域内的类型名相同(例如不应该在命名空间Foo下定义Foo类型)。
示例:
namespace Microsoft.Office.PowerPoint { }
4.3 namespace与using位置
C#对使用namespace与using语句出现的位置有一些要求,通常一个可能的顺序如下:
global using System; // 全局using指令
namespace Alpha; // 作用于文件范围的命名空间
using Alpha; // using指令
using IntList = System.Collections.Generic.List<int>; // using别名
namespace Beta // 普通命名空间
{
}
具体的顺序不需要刻意记忆,若顺序不符合要求编译器会给出提示。
4.4 完全限定命名空间我们将以全局命名空间为根空间表示的命名空间称为‘完全限定命名空间’,例如对于以下命名空间:
namespace A
{
namespace B
{
namespace C
{
}
}
}
如果要表示上述命名空间声明下的命名空间C,可以使用A.B.C,但如果此时位于命名空间A中,那么也可以使用B.C,但是其中只有A.B.C这一表示才是表示的完全限定命名空间。通过完全限定命名空间可以从全局命名空间开始准确定位到某一命名空间。
4.5 隐式全局命名空间导入现在新建C#项目后,你会发现项目的csproj文件里有这样一行配置:
<ImplicitUsings>enable</ImplicitUsings>
当项目开启ImplicitUsings时,其作用相当于为你的项目引入了一个对常用命名空间进行全局导入的文件,也就是说相当于在你的项目中加入了有类似如下内容的文件:
global using System;
global using System.Collections.Generic;
...
这一功能是对全局using指令的实际应用,参照于此,你也可以定义一个全局导入自己常用的命名空间的文件,并按需要添加到自己的项目中。
4.6 误区虽然本文最开始的例子中,我们为C语言假想的using语句的作用是‘视接下来所有的函数都有某一前缀’,C#中的命名空间的表现似乎也确实如此。然而,仅仅是这么认为的话会让人误认为下面的代码可以通过编译:
文件1内容:
namespace A.B
{
class Foo { }
}
文件2内容:
using A;
namespace Test
{
class Program
{
static void Main(string[] args)
{
B.Foo foo = new B.Foo(); // 看起来B.Foo在添加using的A.前缀后,就是A.B.Foo了
}
}
}
在上述文件2中,使用using声明导入了命名空间A,然后在Main方法中尝试使用B.Foo来表示A.B.Foo类型。然而这是无法通过编译的,如果这一行为允许,那么考虑下面代码:
文件1内容:
namespace A.B
{
class Foo { }
}
namespace B
{
class Foo { }
}
文件2内容:
using A;
namespace Test
{
class Program
{
static void Main(string[] args)
{
B.Foo foo = new B.Foo(); // 此时的foo是B.Foo还是A.B.Foo
}
}
}
由于Test名称空间下没有B.Foo的匹配项,因此现在编译器会从using声明导入过的名称空间中查找类型。问题在于此时的B.Foo到底是作为B.Foo的完整类型名,还是A.B.Foo的部分类型名?为了避免这一令人迷惑的情况,C#不允许上述做法。可以认为,当你决定在类型名中加入命名空间,并且类型所属的命名空间不是当前命名空间的子空间,就只能使用其完整类型名。如果类型所属的命名空间是当前命名空间的子空间则可以使用子空间名.类型名
来指示类型:
namespace B
{
class Foo
{
public void Tell() { }
}
}
namespace A
{
namespace B
{
class Foo { }
}
class Program
{
static void Main(string[] args)
{
B.Foo foo = new B.Foo(); // B.Foo的完整类型名是A.B.Foo
foo.Tell();
}
}
}
由于编译器会优先以当前命名空间为起点查找类型,而上述情况下编译器可以从命名空间A下查找到B.Foo,故会选择优先使用。如果要使用完整类型名为B.Foo的类型,则添加使用global::即可。
4.7 使用建议(1)一个文件中应只声明一个命名空间
(2)尽可能避免用嵌套声明命名空间,而是使用句点.表示命名空间的嵌套关系:
namespace Alpha // 嵌套声明命名空间
{
namespace Beta
{
}
}
namespace Alpha.Beta // 使用.来表示命名空间嵌套关系
{
}
(3)灵活使用Using别名来避免不必要的类型定义与简化类型名
using IntList = System.Collections.Generic.List<int>; // 表示一个Int列表,但没有额外的类型定义,同时简化了类型名
(4)规范导入命名空间的顺序,例如可以按照命名空间的名称导入,或者按照先内置库→第三方库→当前项目的顺序导入等等