“使用”指令应该在命名空间内部还是外部?

我一直在一些C#代码上运行StyleCop,它不断报告我的using指令应该在命名空间内。

using指令放在命名空间内部而不是外部是否有技术原因?

260650 次浏览

将它放在命名空间中会使声明在文件的命名空间本地(如果文件中有多个命名空间),但如果每个文件只有一个命名空间,那么它们是在命名空间外部还是内部并没有太大的区别。

using ThisNamespace.IsImported.InAllNamespaces.Here;
namespace Namespace1{using ThisNamespace.IsImported.InNamespace1.AndNamespace2;
namespace Namespace2{using ThisNamespace.IsImported.InJustNamespace2;}}
namespace Namespace3{using ThisNamespace.IsImported.InJustNamespace3;}

根据Hanselman-使用指令和装配加载…和其他此类文章,在技术上没有区别。

我的偏好是将它们放在命名空间之外。

这两者之间实际上存在(微妙的)差异。假设您在File1.cs中有以下代码:

// File1.csusing System;namespace Outer.Inner{class Foo{static void Bar(){double d = Math.PI;}}}

现在假设有人向项目添加了另一个文件(File2.cs),如下所示:

// File2.csnamespace Outer{class Math{}}

编译器在查看命名空间之外的那些using指令之前搜索Outer,因此它找到Outer.Math而不是System.Math。不幸的是(或者幸运的是?),Outer.Math没有PI成员,所以File1现在坏了。

如果将using放在命名空间声明中,则会发生变化,如下所示:

// File1b.csnamespace Outer.Inner{using System;class Foo{static void Bar(){double d = Math.PI;}}}

现在编译器在搜索Outer之前搜索System,找到System.Math,一切都很好。

有些人会争辩说,对于用户定义的类来说,Math可能是一个不好的名字,因为System中已经有一个;这里的重点只是存在差异,它会影响代码的可运维性。

注意如果Foo在命名空间Outer而不是Outer.Inner中会发生什么也很有趣。在这种情况下,在File2中添加Outer.Math会破坏File1,无论using在哪里。这意味着编译器在查看任何using指令之前搜索最里面的封闭命名空间。

根据StyleCop文档:

SA1200: Using DirectivesMustBePlacedWith inNamespace

原因使用C#的指令放在命名空间元素之外。

规则说明违反此规则的情况下,使用指令或using-alias指令放置在名称空间元素之外,除非该文件不包含任何名称空间元素。

例如,以下代码将导致两次违反此规则。

using System;using Guid = System.Guid;
namespace Microsoft.Sample{public class Program{}}

但是,以下代码不会导致任何违反此规则的行为:

namespace Microsoft.Sample{using System;using Guid = System.Guid;
public class Program{}}

这段代码将编译得很干净,没有任何编译器错误。但是,不清楚分配的是哪个版本的Guid类型。如果在命名空间内移动use指令,如下所示,将发生编译器错误:

namespace Microsoft.Sample{using Guid = System.Guid;public class Guid{public Guid(string s){}}
public class Program{public static void Main(string[] args){Guid g = new Guid("hello");}}}

代码在以下编译器错误中失败,该错误位于包含Guid g = new Guid("hello");的行中

CS0576:命名空间'Microsoft. Sample'包含与别名'Guide'冲突的定义

这段代码为System. Guid类型创建了一个名为Guid的别名,并且还创建了它自己的名为Guid的类型,并使用匹配的构造函数接口。稍后,这段代码创建了一个Guid类型的实例。要创建这个实例,编译器必须在Guid的两个不同定义之间进行选择。当use-alias指令放在命名空间元素之外时,编译器将选择在本地命名空间内定义的Guid的本地定义,并完全忽略在命名空间之外定义的use-alias指令。不幸的是,这在阅读代码时并不明显。

但是,当using-alias指令位于命名空间中时,编译器必须在定义在同一命名空间中的两种不同且相互冲突的Guid类型之间进行选择。这两种类型都提供了匹配的构造函数。编译器无法做出决定,因此它会标记编译器错误。

将using-alias指令放在命名空间之外是一种不好的做法,因为在这样的情况下,它可能会导致混淆,在这种情况下,不清楚实际使用的是哪个版本的类型。这可能会导致难以诊断的bug。

在命名空间元素中放置using-alias指令可以消除这一错误来源。

  1. 多个命名空间

将多个命名空间元素放置在单个文件中通常是一个坏主意,但是如果这样做了,最好将所有use指令放置在每个命名空间元素中,而不是全局放置在文件顶部。这将严格限定命名空间的范围,也将有助于避免上面描述的那种行为。

重要的是要注意,当代码使用放置在命名空间之外的use指令编写时,在命名空间内移动这些指令时应该小心,以确保这不会改变代码的语义学。如上所述,将using-alias指令放置在命名空间元素中允许编译器在冲突类型之间进行选择,而当指令放置在命名空间之外时不会发生这种情况。

如何修复违规行为若要修复违反此规则的问题,请在命名空间元素中移动所有使用指令和使用别名指令。

当您希望使用别名时,在命名空间中放置使用语句存在问题。别名不能从早期的using语句中受益,必须完全限定。

考虑:

namespace MyNamespace{using System;using MyAlias = System.DateTime;
class MyClass{}}

对:

using System;
namespace MyNamespace{using MyAlias = DateTime;
class MyClass{}}

如果你有一个冗长的别名,这可能特别明显,如下所示(这就是我发现问题的方式):

using MyAlias = Tuple<Expression<Func<DateTime, object>>, Expression<Func<TimeSpan, object>>>;

在命名空间中使用using语句,它突然变成:

using MyAlias = System.Tuple<System.Linq.Expressions.Expression<System.Func<System.DateTime, object>>, System.Linq.Expressions.Expression<System.Func<System.TimeSpan, object>>>;

不漂亮。

这个帖子已经有了一些很好的答案,但我觉得我可以用这个额外的答案带来更多的细节。

首先,请记住带有句点的命名空间声明,例如:

namespace MyCorp.TheProduct.SomeModule.Utilities{...}

完全等同于:

namespace MyCorp{namespace TheProduct{namespace SomeModule{namespace Utilities{...}}}}

如果你愿意,你可以把using指令放在所有这些级别上。(当然,我们希望只在一个地方有using,但根据语言,这是合法的。)

解析隐含类型的规则可以像这样松散地陈述:首先搜索最内部的“范围”进行匹配,如果没有找到任何匹配,则从一个级别到下一个范围并在那里搜索,依此类推,直到找到匹配项。如果在某种程度上找到了多个匹配项,如果其中一个类型来自当前程序集,请选择那个并发出编译器警告。否则,放弃(编译时错误)。

现在,让我们在两个主要约定的具体示例中明确说明这意味着什么。

(1)外部使用:

using System;using System.Collections.Generic;using System.Linq;//using MyCorp.TheProduct;  <-- uncommenting this would change nothingusing MyCorp.TheProduct.OtherModule;using MyCorp.TheProduct.OtherModule.Integration;using ThirdParty;
namespace MyCorp.TheProduct.SomeModule.Utilities{class C{Ambiguous a;}}

在上面的例子中,为了找出类型Ambiguous是什么,搜索按以下顺序进行:

  1. C内部的嵌套类型(包括继承的嵌套类型)
  2. 当前命名空间中的类型MyCorp.TheProduct.SomeModule.Utilities
  3. 名称空间中的类型MyCorp.TheProduct.SomeModule
  4. 类型MyCorp.TheProduct
  5. 类型MyCorp
  6. null命名空间(全局命名空间)中的类型
  7. SystemSystem.Collections.GenericSystem.LinqMyCorp.TheProduct.OtherModuleMyCorp.TheProduct.OtherModule.IntegrationThirdParty中的类型

其他公约:

(2)内部使用:

namespace MyCorp.TheProduct.SomeModule.Utilities{using System;using System.Collections.Generic;using System.Linq;using MyCorp.TheProduct;                           // MyCorp can be left out; this using is NOT redundantusing MyCorp.TheProduct.OtherModule;               // MyCorp.TheProduct can be left outusing MyCorp.TheProduct.OtherModule.Integration;   // MyCorp.TheProduct can be left outusing ThirdParty;
class C{Ambiguous a;}}

现在,搜索类型Ambiguous按以下顺序进行:

  1. C内部的嵌套类型(包括继承的嵌套类型)
  2. 当前命名空间中的类型MyCorp.TheProduct.SomeModule.Utilities
  3. SystemSystem.Collections.GenericSystem.LinqMyCorp.TheProductMyCorp.TheProduct.OtherModuleMyCorp.TheProduct.OtherModule.IntegrationThirdParty中的类型
  4. 名称空间中的类型MyCorp.TheProduct.SomeModule
  5. 类型MyCorp
  6. null命名空间(全局命名空间)中的类型

(请注意,MyCorp.TheProduct是“3.”的一部分,因此在“4.”和“5.”之间不需要。

结束语

无论您将用法放在命名空间声明内部还是外部,总是有可能有人稍后将具有相同名称的新类型添加到具有更高优先级的命名空间之一。

此外,如果嵌套命名空间与类型具有相同的名称,则可能会导致问题。

将用法从一个位置移动到另一个位置总是很危险的,因为搜索层次结构会发生变化,并且可能会找到另一种类型。因此,选择一个约定并坚持它,这样你就不必移动用法了。

默认情况下,Visual Studio的模板将名称空间的用法外面(例如,如果您让VS在新文件中生成一个新类)。

使用usings外面的一个(微小)优点是,您可以将use指令用于全局属性,例如[assembly: ComVisible(false)]而不是[assembly: System.Runtime.InteropServices.ComVisible(false)]


更新文件范围的命名空间声明

从C#10.0(从2021年开始)开始,你可以避免缩进并使用以下任何一种(约定1,外部使用):

using System;using System.Collections.Generic;using System.Linq;using MyCorp.TheProduct.OtherModule;using MyCorp.TheProduct.OtherModule.Integration;using ThirdParty;
namespace MyCorp.TheProduct.SomeModule.Utilities;
class C{Ambiguous a;}

或(约定2,内部用法):

namespace MyCorp.TheProduct.SomeModule.Utilities;
using System;using System.Collections.Generic;using System.Linq;using MyCorp.TheProduct;using MyCorp.TheProduct.OtherModule;using MyCorp.TheProduct.OtherModule.Integration;using ThirdParty;
class C{Ambiguous a;}

但与之前相同的考虑适用。

如果在源解决方案中使用“参考文献”的那些默认应该在命名空间之外,那么这是一个更好的做法,而那些"新增引用"是一个很好的做法,您应该将其放在命名空间内。这是为了区分正在添加的引用。

作为Jeppe Stig Nielsen,这个线程已经有了很好的答案,但我认为这个相当明显的微妙之处也值得一提。

在命名空间内指定的using指令可以缩短代码,因为它们不需要像在外部指定时那样完全限定。

以下示例有效,因为类型FooBar都在同一个全局命名空间Outer中。

假设代码文件Foo.cs

namespace Outer.Inner{class Foo { }}

Bar.cs

namespace Outer{using Outer.Inner;
class Bar{public Foo foo;}}

这可能会省略using指令中的外部命名空间,简称:

namespace Outer{using Inner;
class Bar{public Foo foo;}}

技术原因在答案中进行了讨论,我认为这最终取决于个人偏好,因为区别不是,并且两者都有权衡。Visual Studio创建.cs文件的默认模板使用using名称空间之外的指令,例如:

您可以通过在项目文件的根目录中添加stylecop.json文件来调整style letop以检查名称空间之外的using指令,如下所示:

{"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json","orderingRules": {"usingDirectivesPlacement": "outsideNamespace"}}}

您可以在解决方案级别创建此配置文件,并将其作为“现有链接文件”添加到您的项目中,以便在您的所有项目中共享配置。

我不相信其他答案已经涵盖的另一个微妙之处是当您有一个同名的类和命名空间时。

当您在命名空间中导入时,它会找到该类。如果导入在命名空间之外,则导入将被忽略,并且类和命名空间必须完全限定。

//file1.csnamespace Foo{class Foo{}}
//file2.csnamespace ConsoleApp3{using Foo;class Program{static void Main(string[] args){//This will allow you to use the classFoo test = new Foo();}}}
//file3.csusing Foo; //Unused and redundantnamespace Bar{class Bar{Bar(){Foo.Foo test = new Foo.Foo();Foo test = new Foo(); //will give you an error that a namespace is being used like a class.}}}

我遇到的一个皱纹(其他答案中没有涵盖):

假设你有这些命名空间:

  • 别的东西
  • 父母,别的什么

当您使用namespace Parent中的using Something.Other外面时,它指的是第一个(某些东西)。

但是,如果您在该命名空间声明中使用它里面,它指的是第二个(父母.某事.其他)!

有一个简单的解决方案:添加“global::”前缀:文档

namespace Parent{using global::Something.Other;// etc}

通常,外部using指令(例如System和Microsoft命名空间)应放在namespace指令的外面位置。它们是默认值,应应用于所有情况除非另有说明。这应包括您自己组织的任何不属于当前项目的内部库,或引用同一项目中其他主要命名空间的using指令。任何引用当前项目和命名空间中其他模块的using指令都应放在里面namespace指令中。这有两个特定功能:

  • 它提供了本地模块和“其他”模块之间的视觉区别,即其他所有模块。
  • 它限定要应用优先而不是全局指令的本地指令的范围。

后一个原因很重要。这意味着很难引入一个模棱两可的引用问题,而这个问题可能是由一个不超过重构代码的更改引入的。也就是说,你将一个方法从一个文件移动到另一个文件,突然出现了一个以前不存在的bug。通俗地说,一个“heisenbug”-历史上非常难以追踪。

尚未提及:

using指令<强>内放在命名空间声明中是在尽可能小的范围内声明一切的著名最佳编程实践的应用。

如果最佳编程实践是您的第二天性,那么您会自动执行此类操作。

这可能是在命名空间声明中放置使用指令的最佳原因,而不管其他地方提到的(边界线)技术(边界线)优点;就这么简单。


尚未提及:

如果您在每个模块中复制了某个类(例如internal static class Statics),那么使用指令<强>内命名空间声明,您可以始终使用相同的using指令(例如using static Statics;)包含当前模块的类,而无需担心提供正确的命名空间。


已经提到,但也许更好的说明:

using指令<强>内放在命名空间中可以避免不必要的重复,并使我们的声明更加简洁。

这是不必要的简洁:

using Com.Acme.Products.Traps.RoadRunnerTraps;namespace Com.Acme.Products.Traps {

这是甜蜜和关键:

namespace Com.Acme.Products.Traps {using RoadRunnerTraps;