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

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


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

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.cs
using System;
namespace Outer.Inner
{
    class Foo
    {
        static void Bar()
        {
            double d = Math.PI;
        }
    }
}

现在想象一下,有人将另一个文件(File2.cs)添加到项目中,如下所示:

// File2.cs
namespace Outer
{
    class Math
    {
    }
}

编译器在查看命名空间外使用指令的对象之前搜索Outer,因此找到Outer.Math而不是System.Math。不幸的是(或者幸运的是?),Outer.Math没有PI成员,因此File1现在已损坏。

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

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

现在编译器先搜索System,再搜索Outer,找到System.Math,一切正常。

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

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


根据StyleCop文档:

SA1200:使用方向必须放置在命名空间中

原因C#using指令位于命名空间元素的外部。

规则说明如果将using指令或using别名指令放置在命名空间元素之外,则会违反此规则,除非文件不包含任何命名空间元素。

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

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类型的哪个版本。如果将using指令移到命名空间内部,如下所示,将发生编译器错误:

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”包含与别名“Guid”冲突的定义

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

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

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

在命名空间元素中放置使用别名指令可以消除这一错误源。

多个命名空间

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

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

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


当您希望使用别名时,在名称空间中放置using语句存在问题。别名不能从前面的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 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
            {
                ...
            }
        }
    }
}

如果您愿意,可以在所有这些级别上使用指令。(当然,我们只想在一个地方使用,但根据语言的不同,这是合法的。)

解析隐含类型的规则可以大致如下:首先在最内部的“范围”中搜索匹配项,如果没有找到匹配项,则从一个级别转到下一个范围,然后在那里搜索,依此类推,直到找到匹配项。如果在某个级别上找到多个匹配项,如果其中一个类型来自当前程序集,请选择该类型并发出编译器警告。否则,放弃(编译时错误)。

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

(1) 外部使用:

using System;
using System.Collections.Generic;
using System.Linq;
//using MyCorp.TheProduct;  <-- uncommenting this would change nothing
using MyCorp.TheProduct.OtherModule;
using MyCorp.TheProduct.OtherModule.Integration;
using ThirdParty;

namespace MyCorp.TheProduct.SomeModule.Utilities
{
    class C
    {
        Ambiguous a;
    }
}

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

C内部的嵌套类型(包括继承的嵌套类型)当前命名空间MyCorp.TheProduct.SomeModule.Utilities中的类型命名空间MyCorp.TheProduct.SomeModule中的类型MyCorp.TheProduct中的类型MyCorp中的类型空命名空间(全局命名空间)中的类型System、System.Collections.Generic、System.Linq、MyCorp.TheProduct.OtherModule、MyCorp.TheProduct.Other Module.Integration和ThirdParty中的类型

其他约定:

(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 redundant
    using MyCorp.TheProduct.OtherModule;               // MyCorp.TheProduct can be left out
    using MyCorp.TheProduct.OtherModule.Integration;   // MyCorp.TheProduct can be left out
    using ThirdParty;

    class C
    {
        Ambiguous a;
    }
}

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

C内部的嵌套类型(包括继承的嵌套类型)当前命名空间MyCorp.TheProduct.SomeModule.Utilities中的类型System、System.Collections.Generic、System.Linq、MyCorp.TheProduct、MyCorp.TheProduct.OtherModule、MyCorp.TheProduct.OtherModule.Integration和第三方中的类型命名空间MyCorp.TheProduct.SomeModule中的类型MyCorp中的类型空命名空间(全局命名空间)中的类型

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

结束语

无论您将using放在名称空间声明的内部还是外部,都有可能有人稍后将具有相同名称的新类型添加到其中一个具有较高优先级的名称空间中。

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

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

默认情况下,VisualStudio的模板将using放在命名空间之外(例如,如果您使VS在新文件中生成新类)。

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


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

由于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)所说,这条线索已经有了很好的答案,但我认为这一相当明显的微妙之处也值得一提。

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

以下示例之所以有效,是因为类型Foo和Bar都位于同一个全局命名空间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文件的默认模板使用命名空间之外的指令,例如。

通过在项目文件的根目录中添加stylecop.json文件,可以调整stylecop以使用命名空间之外的指令进行检查,如下所示:

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

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


另一个我认为没有被其他答案涵盖的微妙之处是,当你有一个同名的类和命名空间时。

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

//file1.cs
namespace Foo
{
    class Foo
    {
    }
}

//file2.cs
namespace ConsoleApp3
{
    using Foo;
    class Program
    {
        static void Main(string[] args)
        {
            //This will allow you to use the class
            Foo test = new Foo();
        }
    }
}

//file3.cs
using Foo; //Unused and redundant    
namespace 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.
        }
    }
}

我遇到了一条皱纹(其他答案中没有提到):

假设您有以下名称空间:

有些东西。其他父级.某些.其他

当您在命名空间Parent之外使用Something.Other时,它指的是第一个(Something.Other)。

但是,如果在该命名空间声明中使用它,它将引用第二个(Parent.Something.Other)!

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

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

通常,外部using指令(例如System和Microsoft命名空间)应该放在命名空间指令之外。除非另有规定,否则应在所有情况下应用默认值。这应该包括不属于当前项目的任何组织内部库,或者使用引用同一项目中其他主名称空间的指令。引用当前项目和命名空间中其他模块的任何using指令都应放在命名空间指令中。这有两个特定的功能:

它提供了本地模块和“其他”模块之间的视觉区别,这意味着所有其他模块。它将本地指令的范围限定为优先于全局指令应用。

后一个原因很重要。这意味着很难引入一个不明确的引用问题,而这个引用问题可能是由一个比重构代码更重要的更改引起的。也就是说,你将一个方法从一个文件移动到另一个文件,突然出现了一个以前不存在的bug。通俗地说,一个“海森堡”——历史上极难追踪。


尚未提及:

将using指令放在命名空间声明中是众所周知的最佳编程实践的一个应用,即在尽可能小的范围内声明所有内容。

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

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


已经提到了,但可能更好地说明了:

在名称空间中放置using指令可以避免不必要的重复,并使声明更加简洁。

这是不必要的简洁:

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

这是甜蜜的,切中要害:

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