学习路线指引(点击解锁) | 知识定位 | 人群定位 |
---|---|---|
Python实战微信订餐小程序 | 进阶级 | 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 |
Python量化交易实战 | 入门级 | 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 |
面向C#新学者,介绍命名空间(namespace)的概念以及C#中的命名空间的相关内容。
理解C与C#语言的基础语法。
理解作用域概念。
假设猫猫头在北京有一个叫AAA的朋友,在上海有两个叫AAA的朋友,上海的两个AAA一个喜欢咸粽子,一个喜欢甜粽子。有一天猫猫找朋友玩,朋友问道:
“AAA最近过得怎么样”,
然而猫猫头有三个叫AAA的朋友,因此猫猫头不确定朋友问的是哪个AAA,于是朋友改问:
“上海的那个AAA最近过得怎么样”
精确了一点,但这还不够,因为猫猫头在上海认识两个叫AAA的朋友,于是朋友再次改问:
“上海的那个喜欢咸粽子的AAA最近过得怎么样。
到这里,猫猫头就确定了朋友问的是哪个小明。也就是说,通过地域+喜好+姓名,猫猫头可以确定朋友指的具体的人。
这个例子中,通过一层一层的限定修饰,我们从逐渐精确定位到了指定的AAA。在现实中,通过各种各样的限定修饰,我们可以区分具有相似名称的人或物,而对于程序来说也是如此。
(1)函数命名冲突
在谈论什么是命名空间之前,我们先来看一看C语言中存在的一些问题。假设你和你的小伙伴同时开发一个C程序,并且你们很巧地定义了两个函数名相同的函数:
void Init() { } // 初始化控制台
void Init() { } // 初始化打印机
假设这两个函数做的事完全不同(一个用来初始化控制台(Console),一个用来初始化打印机(Printer))而无法合并,那么显然此时需要用一个办法来区分两个函数。经过简单讨论,你和你的小伙伴决定在每个函数名前添加函数的作用对象名字加以区分,于是你们把函数名改成了如下:
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(); // 在编译器看来是ConsoleInit
Foo(); // 在编译器看来是ConsoleFoo
Whatever(); // 在编译器看来是ConsoleWhatever
Print("..."); // 在编译器看来是ConsolePrint
}
此时就可以不用输入很多不必要的Console前缀,使用函数就方便了许多。
(2)让编译器代劳
基于上述理由,可以定义一种语法来告诉编译器为接下来使用的函数名都添加指定前缀,例如:
// 使用namespace关键字告诉编译器为其后代码块中所有的函数名都添加Console前缀
namespace Console
{
Init();
Foo();
Whatever();
Print("...");
}
在这里,我们设定使用namespace关键字来告诉编译器为后面代码块中所有的函数都添加其后面指定的Console前缀,这样在编译器看来,上述实际代码就如下:
ConsoleInit();
ConsoleFoo();
ConsoleWhatever();
ConsolePrint("...");
显然此时程序依然可以准确地调用合适的函数。同样,既然可以让编译器在调用函数时自动为其添加前缀,那么自然也可以让其在定义函数时也为函数名自动添加前缀:
namespace Console // 为其后代码块中的成员自动添加Console前缀
{
void Init() { ... }
void Foo() { ... }
void Whatever() { ... }
void Print(const char* s) { ... }
}
这样,在编译器进行自动转换后,上述的代码就会像下面这样:
void ConsoleInit() { }
void ConsoleFoo() { }
void ConsoleWhatever() { }
void ConsolePrint(const char* s) { }
有了这种自动添加前缀的语法后,那么在对控制台进行相关的操作时,就可以像下面这样操作了:
// 使用namespace关键字告诉编译器为其后代码块中所有的函数名都添加Console前缀
namespace Console
{
void Init() // 定义init函数(函数全名是ConsoleInit)
{
...
}
void Launch() // 定义Launch函数(函数全名是ConsoleLaunch),并在函数中调用前面定义的Init方法
{
Init(); // 该Init即ConsoleInit
...
}
}
而对打印机进行相关操作时,也只需要:
// 使用namespace关键字告诉编译器为其后代码块中所有的函数名都添加Printer前缀
namespace Printer
{
void Init() // 定义init函数(函数全名是PrinterInit)
{
...
}
void Launch() // 定义Launch函数(函数全名是PrinterLaunch),并在函数中调用前面定义的Init方法
{
Init(); // 该Init即PrinterInit
...
}
}
显然,有了自动添加前缀的语法后,定义和使用函数都方便了许多。
更近一步,还可以再允许使用嵌套语法添加前缀:
namespace MeAndFriend // 为其后代码块中的成员自动添加MeAndFriend前缀
{
namespace Console // 为其后代码块中的成员自动添加Console前缀
{
void Init() { } // 此时Init实际应该叫MeAndFriendConsoleInit
// ...
}
namespace Printer // 为其后代码块中的成员自动添加Printer前缀
{
void Init() { } // 此时Init实际应该叫MeAndFriendPrinterInit
// ...
}
}
这样,例如上述代码就会由编译器生成类似‘MeAndFriendConsoleInit’与‘MeAndFriendPrinterInit’这样本来会很长的函数名。
在上述代码中,我们定义了一个语法来表示一个为某一个代码块中的成员添加前缀:
// 告诉编译器代码块中的成员都默认有Console前缀
namespace Console
{
...
}
此时在编译器看来,所有处于namespace Console后面的代码块作用域中的成员都自带Console前缀。而对作用域来说,通过namespace关键字,我们为其提供了一个名字Console,也就是说,这是一个有名字的作用域,而这种有名字的作用域,就是所谓的命名空间(或者也可以称其为名称空间/名字空间)。命名空间(namespace)中的“命名(name)”部分就是一个限定修饰词,而其“空间(space)”就是这一限定修饰词的作用域。
看起来命名空间似乎不是必须的东西,例如如果是为了避免函数名冲突,那么完全可以手动为函数名添加各种限定词来避开冲突,然而正如前面所看到的,如果每个函数在定义和调用的时候都要输入如此多的和函数所做的事无关的附加信息,那么这对于输入和阅读代码都是额外的负担,并且可能会对以后可能的代码修改带来诸多不便,而命名空间这一概念的出现在很大程度上缓解了这一问题。
命名空间是如此有用的东西,以至于不少现代化的编程语言都有类似命名空间的设计。C#自然也有一套自己的命名空间体系,MSDN上对命名空间的定义是‘包含一组相关对象的作用域’,这一概念有点抽象,接下来我们从具体的使用中来理解。
(1)全局命名空间
默认情况下,存在一个被称为全局命名空间的根空间,这个空间是匿名隐式的,有全局的作用域。因此如果一个类型没有定义在任何声明的命名空间下,则默认其直接位于全局命名空间。
(2)声明命名空间
显然如果只有全局名称空间没有太大的意义,应该还要能声明特定的名称空间,要在C#中声明一个命名空间,只需要使用namespace关键字并加上空间名与一对花括号(即定义代码块)即可,下述代码声明了一个命名空间Alpha:
namespace Alpha
{
}
同样,命名空间也可以嵌套声明:
namespace Alpha
{
namespace Beta
{
}
}
(Beta是嵌套在Alpha中的一个子空间,而Alpha则是Beta的父空间)
不过按照格式规范,如果像上述那样嵌套命名空间的话,在格式化代码样式时会浪费大量的列缩进(每一级代码块中的代码需要缩进4个空格,因此每多一层命名空间就会导致所有代码多缩进4个空格)。因此还可以通过使用句点.
来连接命名空间以表示命名空间的嵌套关系,上述嵌套命名空间也可以采用下述声明方法:
namespace Alpha.Beta
{
}
需要说明的是,所有名称空间都可以视为全局命名空间的子空间,而如果我们从全局命名空间开始书写一个命名空间,则将这一命名空间名称为“完全限定命名空间”。如在上述命名空间的定义下,当表示Beta命名空间时,Alpha.Beta就是一个完全限定命名空间,而Beta则不是完全限定命名空间。另外,全局命名空间虽然是匿名的,但是可以使用global关键字来指代,并在其后使用::
(而不是.
)连接子空间,因此,完全限定命名空间Alpha.Beta也可以表示为:
global::Alpha.Beta
(写过C++的朋友应该有一种**熟悉感)
(2)在命名空间中定义类型
在一个命名空间的代码块作用域内定义的类型都会归属到该命名空间,例如下述代码中,Foo属于Alpha命名空间:
namespace Alpha
{
class Foo
{
}
}
上述代码在命名空间Alpha下定义了一个Foo对象,此时若按我们在前文对命名空间的实际作用的解释来看,用句点.
来连接命名空间与类型名,那么Foo类型的完整名称应该是Alpha.Foo
,也就是说,命名空间的名字和该空间下的类型名可以共同组成一个更为明确的类型名,因此,像下面这样定义不会发生冲突:
namespace Alpha
{
class Foo
{
}
}
namespace Beta
{
class Foo
{
}
}
尽管上述代码中出现了两个名称为Foo的类,但两个Foo的完整名称分别为Alpha.Foo与Beta.Foo,在编译器看来这可以是两个完全不同的类型。类型的完整名为其所属的完全限定命名空间加上类型名,例如对于以下位于嵌套命名空间Alpha.Beta的Foo类:
// 或者简化的嵌套写法
// namespace Alpha.Beta
namespace Alpha
{
namespace Beta
{
class Foo // Alpha.Beta.Foo
{
}
}
}
Foo类型的完整名应该是Alpha.Beta.Foo
而不是Beta.Foo,为了方便后文的阐述,我们将这种‘以完全限定命名空间.类型名
格式表达的类型名’称为‘完整类型名’。
(1)跨命名空间访问
位于同一个命名空间作用域中的类型之间可以直接使用类型名访问,例如:
namespace Alpha
{
class Foo { }
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo(); // 直接使用Foo表示Alpha.Foo
}
}
}
类型Foo的完整类型名是Alpha.Foo,但由于Program类也处在Alpha命名空间作用域内,因此可以直接使用Foo来表示Alpha.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(); // 使用完整类型名
}
}
}
(2):using指令
显然跨命名空间使用类型时使用完整类型名是一件很繁琐的事,C#自然提供了相应的解决方法。对于上面的例子,如果要想如同在Alpha命名空间中一样简单地直接使用Foo表示Alpha.Foo,可以使用using指令来达到这一目的:
using Alpha; // using指令,导入Alpha命名空间
namespace Beta
{
class Program
{
static void Main(string[] args)
{
Foo foo = GetFoo();
}
static Foo GetFoo() { ... }
static void CheckFoo(Foo foo) { ... }
}
}
(默认情况下,由于using指令影响的范围是使用该using指令的整个文件,因此using指令被要求放置在文件开头以清楚描述其行为)
在using关键字后面跟随命名空间名,表示在当前文件中‘使用指定命名空间的作用域’,或者说,把指定命名空间的作用域导入到当前文件,为了方便,后文中将这一行为称为‘导入命名空间’。因此上述代码使用using指令导入命名空间Alpha后,就会使用Alpha的作用域,此时代码就像下图这样:
由于此时代码可以视为在Alpha命名空间的作用域中,因此可以直接使用Foo来表示Alpha.Foo。
另外,可以同时使用多个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 Alpha;
namespace Beta
{
class Foo { } // Beta.Foo
}
Foo的完整类型名依然是Beta.Foo,而不会因为导入了Alpha变成Alpha.Beta.Foo。
(2) 全局using声明
默认的using指令作用域是文件,也就是说一个using指令的声明只对使用了该using指令的这一个文件有效。但有时候一个命名空间可能会频繁用于多个文件,例如System命名空间相当常用,很多文件都需要额外添加using System来导入此命名空间,这有时候会为编码带来枯燥的体验,为此,C#提供了一种名为全局using的导入方法,按此using导入的命名空间会作用于整个项目,只需要在using指令前添加global关键字即可将命名空间其作为全局命名空间导入:
global using System;
在一个项目中的任意一个文件中使用以上using声明后,该项目中所有的文件都会默认导入过System命名空间。另外,语法规定全局using必须位于普通using之前。通常建议将全局using写入到单个文件中。
(3)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;
CatList cats = new CatList();
using别名作用域也是整个文件,因此using别名的声明也要求放在文件开头以清楚描述其行为。
在介绍global关键字之前,需要说一下编译器查找类型的过程:
以具体代码为例:
class Foo { } // 位于全局命名空间的Foo
namespace Alpha
{
class Foo { } // 位于命名空间Alpha的Foo
namespace Beta
{
class Foo { } // 位于命名空间Beta的Foo
class Program // 位于命名空间Beta的Program
{
static void Main(string[] args)
{
Foo foo = new Foo(); // 此时的Foo是Beta.Foo
}
}
}
}
上述代码中Main方法中的Foo是Beta.Foo,原因是当编译器以Main方法所属的Program类所属的命名空间Beta为起点查找类型Foo时,在Beta下就查找到了Foo的定义,于是停止继续向上查找,也就是说,不会继续向上查找到Alpha或全局命名空间中的Foo。此时如果要使用全局命名空间中的Foo,则需要告知编译器应当直接从全局命名空间开始查找(而非当前命名空间),可在通过在类型名前添加global::
达到此目的:
global::Foo foo = new global::Foo(); // 告诉编译器从全局命名空间开始查找,此时Foo就是位于全局命名空间的那个Foo
关于global关键字其实已在前文提过,这里只是提一下它的一些实际作用。
(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。原因在前文提到过,编译器查找类型时会从本命名空间为起点向上查找,此时编译器在命名空间Test下就发现了Foo的定义,故不会继续查找到Alpha命名空间。此时如果要使用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的别名
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方法时,可以像代码在Speaker静态类一样使用Say的方法,某种意义上说,可以认为是将静态类类名视为了一个命名空间。另外,如果类中定义了和静态类中重名的方法,则优先使用类中定义的方法,此时若要使用静态类中的方法依然需要使用类名.方法名来调用:
using static Hello.Speaker; // using static + 静态类的完整类型名
namespace Test
{
class Program
{
static void Main(string[] args)
{
Say(); // 调用的是Program类中的Say
Speaker.Say(); // 此时只能按类名.方法名调用
}
static void Say() { ... } // 和静态类Speaker的Say方法重名
}
}
普通的命名空间声明作用范围是其后面的代码块,但你也可以声明作用于整个文件范围的命名空间,声明后所有在该文件下定义的类型都将纳入此命名空间:
namespace Alpha; // 将Alpha声明为文件作用域的命名空间
class Cat { }
class Dog { }
声明作用于文件范围的命名空间和作用域代码块的命名空间语法相似,其最大的优点在于其可以减少格式化代码样式时所需的列缩进量。需要说明的是,声明作用于文件范围的命名空间有如下限制:
namespace Alpha;
namespace Beta
{
}
查找类型时,编译器会按照下述流程查找类型:
MSDN上给出了命名空间的命名建议:
.(|)[.][.]
示例:
namespace Microsoft.Office.PowerPoint { }
namespace Newtonsoft.Json.Linq { }
C#对使用namespace与using语句出现的位置有一些要求,通常一个可能的顺序如下:
global using System; // 全局using指令
namespace Alpha; // 作用于文件范围的命名空间
using Alpha; // using指令
using IntList = System.Collections.Generic.List; // using别名
namespace Beta // 普通命名空间
{
}
具体的顺序不需要刻意记忆,若顺序不符合要求编译器会给出提示。
现在新建C#项目后,你会发现项目的csproj文件里有这样一行配置:
enableImplicitUsings>
当项目开启ImplicitUsings时,其作用相当于为你的项目引入了一个对常用命名空间进行全局导入的文件,也就是说相当于在你的项目中加入了有类似如下内容的文件:
global using System;
global using System.Collections.Generic;
...
这一功能是对全局using指令的实际应用,参照于此,你也可以定义一个全局导入自己常用的命名空间的文件,并按需要添加到自己的项目中。
虽然本文最开始的例子中,我们为C语言假想的using语句的作用是‘视接下来所有的函数都有某一前缀’,C#中的命名空间的表现似乎也确实如此。然而,仅仅是这么认为的话会让人误认为下面的代码可以通过编译:
文件1内容:
namespace Alpha.Beta
{
class Foo { }
}
文件2内容:
using Alpha;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Beta.Foo foo = new Beta.Foo(); // 看起来Beta.Foo在添加using导入的Alpha后,就是Alpha.Beta.Foo了
}
}
}
在上述文件2中,使用using声明导入了命名空间Alpha,然后在Main方法中尝试使用Beta.Foo来表示Alpha.Beta.Foo类型。这样看上去似乎没什么问题,然而这是无法通过编译的,不应该认为编译器会将using导入的命名空间和类型名中的名称空间部分进行组合(如认为Alpha和Beta.Foo的Beta会组合成Alpha.Beta),因为这可能引起歧义,考虑下面代码:
文件1内容:
namespace Alpha.Beta
{
class Foo { }
}
namespace Beta
{
class Foo { }
}
文件2内容:
using Alpha;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Beta.Foo foo = new Beta.Foo(); // 此时的foo是Beta.Foo还是Alpha.Beta.Foo
}
}
}
上述代码中的Beta.Foo到底是作为Beta.Foo的完整类型名,还是Alpha.Beta.Foo的部分类型名?显然这是不明确的,为了避免这一令人迷惑的情况,编译器不会对命名空间进行自动组合。可以认为,如果要使用using导入的命名空间中的类型,就只能使用不带任何命名空间要素的类型名。
(1)一个文件中应只声明一个命名空间
(2)尽可能避免用嵌套声明命名空间,而是使用句点.表示命名空间的嵌套关系:
namespace Alpha // 嵌套声明命名空间
{
namespace Beta
{
}
}
namespace Alpha.Beta // 使用.来表示命名空间嵌套关系
{
}
(3)灵活使用Using别名来避免不必要的类型定义与简化类型名
using IntList = System.Collections.Generic.List; // 表示一个Int列表,但没有额外的类型定义,同时简化了类型名
(4)规范导入命名空间的顺序,例如可以按照命名空间的名称导入,或者按照先内置库→第三方库→当前项目的顺序导入等等