"CompanyName.Foo" 是一个命名空间,但被用作类型。

47

问题的再陈述

我重新提出这个问题,因为今天我又遇到了这个错误,我仍然完全不明白为什么C#编译器会在没有命名空间存在的情况下检查命名空间和类型之间的冲突。

如果我有...

public Foo MyFoo { get; set; }

为什么编译器会关心 Foo 同时是命名空间和类型呢?你能把属性声明为命名空间而不是类型吗?

"命名空间用作类型" 的编译器错误背后的逻辑是什么?这可以帮我解决哪些问题?

[我该如何标记 Eric Lippert? :)]


原始问题


问题描述

我有一个名为 "Foo" 的项目,其默认命名空间为 CompanyName.Foo。我还有一个叫 "Foo" 的数据库。

当我在数据库上运行 SqlMetal.exe 时,它会生成一个名为 CompanyName.Foo.Models.Foo 的类。

然后,当我尝试创建一个以此类作为类型的属性时,就像这样...

using CompanyName.Foo.Models;
...
public Foo DataContext { get; set; }

我遇到了错误:

'CompanyName.Foo' 是一个 'namespace',但被当作一个 'type' 使用。

我不得不这样做...

public CompanyName.Foo.Models.Foo Foo { get; set; } // :-(

问题:

  1. 为什么会出现这个错误?我的属性声明中并没有包含CompanyName,为什么会有问题?简单来说:Foo != CompanyName.Foo。另外,为了确定,我在整个解决方案中搜索了一下namespace Foo,但没有找到任何结果(如果我实际上使用了命名空间Foo,我就可以理解为什么会出错)。

  2. [已回答] 是否有任何方法可以避免每次使用Foo时都要完整限定它?

  3. [已回答] 是否有任何方法可以让 SqlMetal 给类命名为除Foo以外的其他名称(而不改变我的数据库名称)?我可以使用开关更改命名空间,但我不知道如何更改实际的类名。

更新

仍在寻找(1)的答案。

O.K.W. 解决了(2)和(3)。

Using 声明

请求列出所有我的 using 语句:

using System;
using System.ComponentModel;
using System.Data.Linq;
using System.Linq;
using MyCompany.Foo.Models;

1
我讨厌C#编译器无法区分差异的事实,它应该能够识别名称用作类名的上下文并且不评估命名空间。希望在C# 4.0中解决这个问题,因为这是一个相当大的负担。 - Brett Ryan
8个回答

76

如何标记Eric Lippert?

如果您有想让我注意的事情,可以使用我博客上的"联系"链接。

我仍然完全不明白为什么C#编译器会在毫无意义的命名空间和类型碰撞的情况下检查它们之间的冲突。

确实,在这里的规则很棘手。巧合的是,两周前我写了一系列关于与此问题密切相关的问题的博客文章;它们将在三月初发布。请关注博客获取详情。

更新:上述文章在此处:

链接

为什么会出现这个错误?

让我用几个问题来重新表述这个问题。

哪些规范部分证明了出现此错误的产生?

我认为其他答案已经对此进行了令人满意的解释。类型解析算法非常规范化。但总结一下:在正确名称的东西“内部”时比从“外部”使用正确名称的东西“更紧密地绑定”。当您说:

using XYZ;
namespace ABC.DEF
{
    class GHI : DEF { }
}

那与

using XYZ;
namespace ABC
{
    namespace DEF 
    {
        class GHI : DEF { }
    }
}

现在我们必须确定DEF的含义。我们从内部向外部看。是否有名为DEF的GHI类型参数?没有。看一下容器。是否有名为DEF的DEF成员?没有。看一下容器。是否有名为DEF的ABC成员?。我们完成了;我们已经确定了DEF的含义,它是一个命名空间。我们在询问“XYZ是否有一个名为DEF的成员?”之前发现了DEF的含义。

哪些设计原则影响了这个设计?

一个设计原则是“名称无论如何使用都表示相同的含义”。语言并不完全遵守这个原则;在同一代码中,同一个名称可能用于指代两个不同的事物。但通常情况下,我们努力使得在同一上下文中看到两次“Foo”时,它表示相同的含义。(请参见The Color Color Problemidentifying violations of the "simple name" rules中的一些详细信息。)
另一个设计原则是“不回溯”。在C#中,我们永远不会说“我看到你使用了一个名称来引用在这个上下文中不能引用的东西。让我放弃名称绑定的结果,重新开始寻找可能有效的东西。”
一个更大的原则支撑着“不回溯”原则,即C#不是一种“猜测用户意图”的语言。您编写了一个程序,在这个程序中,最佳的标识符绑定是一个命名空间,但实际上应该是一个类型。有两种可能性。第一种可能性:您犯了一个错误,希望得到告知以便采取措施进行纠正。第二种可能性:您希望我们选择一个较差的绑定,因此我们应该从所有可能的较差绑定中猜测出您可能想要的那个。
在像JScript这样的语言中,这是一个很好的设计原则——JScript是关于当开发者做一些疯狂的事情时混淆的。但C#不是这种类型的语言;我们从开发者那里清楚地获得的反馈是:告诉我什么地方出了问题,以便我可以修复它
“不回溯”的事情在于它使语言更容易理解。假设您有这样的东西:
namespace XYZ.DEF
{
    public class GHI {}
}
namespace QRS.DEF.GHI
{
    public class JKL { }
}
...
using QRS;
namespace TUV 
{
  using XYZ;
  namespace ABC
  {
    namespace DEF 
    {
        class GHI { }
        class MNO : DEF.GHI.JKL { }
    }
  }
}

计算MNO的基本类型。不使用回溯,我们说“DEF是ABC.DEF”,因此GHI是ABC.DEF.GHI。因此,JKL是ABC.DEF.GHI.JKL,但这种类型不存在,会出现错误。您必须通过提供一种类型名称来修复该错误,以便编译器可以识别您想要的DEF。
如果我们使用回溯,我们需要做什么?我们得到了那个错误,然后我们回溯。XYZ包含DEF吗?是的。它包含GHI吗?是的。它包含JKL吗?不是。再次回溯。QRS包含DEF.GHI.JKL吗?是的。
这样做是可行的,但我们能从它的可行性中逻辑地推断出它就是用户想要的吗?
在这种疯狂的情况下,谁知道呢?我们在其中得到了各种良好的绑定,然后在游戏的最后阶段出现问题。我们偶然发现所需答案的想法似乎非常可疑。
在这里正确的做法不是多次回溯并尝试所有阶段的更糟绑定。正确的做法是说:“伙计,这个查找的最佳匹配结果给出了荒谬的结果;请给我一些不太含糊的东西来处理。”
很不幸,编写一种语言的事实是,如果最佳匹配不起作用,编译器会大声抱怨,这就意味着开发人员经常说“当然,在一般情况下,我希望编译器指出我的所有错误——或者更确切地说,我的所有同事的错误。但对于这个特定的案例,我知道我在做什么,所以请编译器做我想要的而不是我说的。”问题在于,你不能两全其美。你不能同时拥有一个强制执行严格规则的编译器,使可疑代码被积极地识别为错误,同时允许通过编译器启发式来编写疯狂的代码,以便找出“我实际上想表达的意思”,当你写一些编译器合理看作是有歧义或错误的东西时。如果您想了解许多专业开发人员如何强烈反感一种语言设计的影响,该设计积极识别错误而不是猜测开发人员希望选择更糟糕的结果,请参见此文章上的116条评论,关于重载分辨率的次要和相当不重要的方面:(请注意,我不再回答有关此问题的评论;我已经解释了我的立场十多次。如果所有这些解释都无法令人信服,那是因为我不是一个很好的说服者。)
最后,如果你真的想测试自己对C#名称解析规则的理解,尝试这个小谜题。几乎每个人都会做错,或者因为错误的原因而得到正确答案。答案在这里

Eric,这正是我正在寻找的答案,谢谢你。我同意你的看法:“你不能两全其美。”如果我犯了一个错误,我确实希望编译器大声抱怨,并且我认为,当“最接近”的匹配不起作用时,编译器不会去寻找可能的匹配是合理的。然而,如果有一种方法可以显式地表达我的意思,而不必对每个Foo的实例都限定完整的命名空间路径或别名,那就太好了。我想到了using Foo as CompanyName.Models.Fooalias Foo = CompanyName.Models.Foo 这样的东西。 - devuxer
在发布上述评论后不久,我了解到我正在寻找的功能已经存在。C#不仅允许您为命名空间设置别名,还允许您为类型设置别名。因此,对于给定的示例,我可以在类顶部放置类似于using FooModel = CompanyName.Foo.Models.Foo;的内容,并声明属性public FooModel MyFoo - devuxer
2
@DanM:确实如此,但有一些不幸的限制。首先,别名指令仅适用于文件或词法命名空间体;您必须在每个使用它的文件中重复别名。其次,它与通用类型不兼容。using IntList = System.Collections.Generic.List<System.Int32>; 可以工作,但 using Foo<T> = Bar<T>; 不行。第三,IntelliSense 有时会出错;如果您有类似 using HANDLE=System.Int32; 的东西,那么 IntelliSense 有时会在您不希望使用 int 时使用 HANDLE - Eric Lippert

11
  1. 冲突发生在名称空间 CompanyName.FooCompanyName.Foo.Models.Foo 之间,而非 Foo。虽然我不确定编译器为什么无法区分两者。

  2. 你可以尝试使用命名空间别名来缩短完整限定的 Foo
    例如:using coyModels = CompanyName.Foo.Models

  3. 参考资料中,似乎你可以使用 /context:<type>/namespace:<name> 来指定数据上下文类(而不是使用表名)和名称空间。


1
谢谢让我再次查看参考文献。我不知道我怎么错过了/context。另外,命名空间别名是一个很好的技巧 - 我不知道你可以这样做。不过,我并没有理解你对于问题(1)的回答。如果两个不匹配的东西之间如何发生冲突呢? - devuxer
我猜 /context 不太直观,我会期望是 /class/type,对吧?至于第一点我的回答,我认为你可以忽略它,因为我也感到困惑。 - o.k.w
@DanM:“两个不匹配的东西怎么可能发生冲突呢?”请注意,在C#中可以通过部分命名空间引用对象。如果您在CompanyName命名空间中编写代码,则可以通过Foo.Something引用Foo子命名空间中的某些内容,而不是CompanyName.Foo.Something。因此,如果您使用CompanyName.Foo.Models,则该命名空间中的类Foo会与您也可以访问的“Foo”子命名空间发生冲突。编译器不知道您想要DataContext属性的Foo子命名空间还是Foo类。 - jrista
你的第一点解释得很清楚。如果你只是单独使用Foo作为类型,编译器就不知道该使用命名空间CompanyName.Foo还是你的类CompanyName.Foo.Models.Foo。当然有人会问:“那么为什么编译器不能聪明地知道我何时将其用作类型,比如:public Foo GetFoo()?”很显然,这无法作为命名空间,那为什么不能将其解释为类型呢?也许Eric已经回答了这个问题,而我只是太懒去读他的非常长的回答。 :) - goku_da_master

5
C#编译器在类和同名命名空间之间存在歧义时不会进行编译。不幸的是,您只需显式地将类命名空间化或重新命名数据库。在您的情况下,编译器甚至没有达到冲突点,在解决 Foo 作为命名空间后就停止了运行。

每当您遇到这种情况:

using CompanyName.Foo.Models;

namespace CompanyName.Foo {
    class Test {
        public Foo Model { get; set; } // error CS0118: 'CompanyName.Foo' is a 'namespace' but is used like a 'type'
        public Foo1 Model { get; set; } //OK
    }
}

namespace CompanyName.Foo.Models {
    class Foo1 {
    }
    class Foo {
    }
}

每个前置命名空间级别在每个级别上都会被隐式导入。这是有道理的,因为使用点嵌套命名空间语法与嵌套命名空间相同。
namespace CompanyName {
    using CompanyName; //<--using1 - Implicit using, since we should be able to access anything within CompanyName implicitly.
    namespace Foo {
        using CompanyName.Foo; //<-- using2 Same as above
        class Test {
            public Foo Model { get; set; } //At this stage due to using1 Foo is actually CompanyName.Foo, hence the compiler error
        }
    }
}

所以在Test类中有两个隐式的using

using CompanyName;
using CompanyName.Foo;

因此,Foo被解析为命名空间,因此出现错误。 编辑:好的观点。我从MSDN挖掘了这个信息:
一个命名空间或类型名称的含义如下确定:
- 如果命名空间或类型名称由单个标识符组成: - 如果命名空间或类型名称出现在类或结构声明的主体中,则从该类或结构声明开始,并继续使用每个封闭类或结构声明(如果有),如果存在具有给定名称的成员,可访问并表示类型,则命名空间或类型名称引用该成员。请注意,在确定命名空间或类型名称的含义时,将忽略非类型成员(常量、字段、方法、属性、索引器、运算符、实例构造函数、析构函数和静态构造函数)。 - 否则,从包含命名空间或类型名称的命名空间开始,继续使用每个封闭命名空间(如果有),并以全局命名空间结束,直到找到实体为止: - 如果命名空间包含具有给定名称的命名空间成员,则命名空间或类型名称引用该成员,并根据成员分类为命名空间或类型。 - 否则,如果命名空间具有与命名空间或类型名称出现的位置相邻的相应命名空间声明,则: - 如果命名空间声明包含将给定名称与导入的命名空间或类型相关联的using-alias-directive,则命名空间或类型名称引用该命名空间或类型。 - 否则,如果由命名空间声明的using-namespace-directives导入的命名空间中恰好包含一个具有给定名称的类型,则命名空间或类型名称引用该类型。
这意味着在解析Foo时,先匹配CompanyName.Foo(第一个粗体部分),然后再匹配using指令(第二个粗体部分)。

...但是类和命名空间的名称不同。这就是我提出问题的全部原因。Foo != CompanyName.Foo - devuxer
谢谢您的解释,但它并没有完全回答“编译器为什么认为在处理'public Foo DataContext { get; set; }'时,'Foo'是指代一个命名空间的问题。为什么编译器会默认我试图将属性作为命名空间而不是类型进行输入?这一点是没有意义的。编译器应该看到'public SomeType PropertyName { get; set; }'并检查是否存在与'SomeType'相匹配的任何类型-而不是检查'SomeType'是否是一个命名空间。 - devuxer
Igor,再次感谢。这似乎是对编译器正在做什么的解释,但它并没有解释为什么。当只有一个标识符存在时,为什么会匹配到命名空间而不是类型?这对我来说毫无意义。 - devuxer
1
好的,既然在语言规范中有这样的规定,那一定有很好的理由。我认为这是为了保持一致性,使得Foo始终解析为相同的内容,无论是命名空间还是类。否则,您将会发现相同的标识符在不同的上下文中意义不同,特别是考虑到整个“命名空间或类”事物的流畅性。 - Igor Zevaka

1
为什么你就不能这样做?
using CompanyName.Foo; 
... 
public Models.Foo DataContext { get; set; } 

1
我是c#的新手,当我反编译一个c#应用程序并将其保存为一个项目后,尝试立即重新编译时,遇到了这个错误...为什么应用程序在第一次编译时能够通过,对我来说仍然是个谜..然而,问题和解决方案非常简单:默认情况下,在添加新类时,c#会使用与命名空间相同的名称作为该命名空间中类的名称!!!!!这很糟糕,因为没有一些隐藏的标识符明确告诉你正在引用哪个(命名空间或类型),编译器就无法区分它们!!!!噢!干得好c#!!!...解决方案:不要重命名成千上万的东西并仔细检查所有更改,运行项目,当您面前有错误列表时,依次单击每个错误以转到每个问题。一旦到达所讨论的“foo”输入点后,在“foo”后面键入一个点(.)使其显示为:foo. ..这应该会弹出包含的类的菜单。在此列表中,双击“foo”(或只需重新输入名称)将原始的“foo”更改为“foo.foo”...对于每个错误都这样做,问题就解决了!哇!我对一个具有复杂名称的整个应用程序都这样做了,效果很好!愉快的编码!- Tim H.

1

我在引用一个类库中的类时遇到了这个问题,其中它的类型与命名空间的根名称相同。最初,在单独的控制台应用程序项目中引用此类型时,没有问题,一切都编译得很好。然而,从Windows服务项目的引用生成了“是'namespace'但被用作'type'”的消息。结果发现,Windows服务项目的目标框架设置为“.NET Framework 4 Client Profile”。将其更改为“.NET Framework 4”即可消除错误。希望这能帮助处于类似情况的人。


0
因为您使用了点符号来分隔Company和Foo,所以您隐式地创建了一个名为Company的命名空间,其中包含一个嵌套的Foo命名空间,而不是您所认为的Company.Foo。
这就是为什么这个不起作用的原因:
namespace Company.Foo
{
}

namespace Company.Foo.Models
{
    public class TestClass {
        public Foo DataContext { get; set; }
    }
}

最接近Foo的是Company命名空间中的嵌套Foo命名空间。但是你可以这样做:

using Company.Foo;
using Company.Foo.Models;

namespace Company.Foo
{
    class Program {
        public static void Main() {}
    }
}

namespace Company.Foo.Models
{
    public class Foo { }

}

public class DataContextClass
{
    public Foo DataContext { get; set; } /* Foo here is automatically resolved to Company.Foo.Models.Foo */
}

编辑

Igor说了同样的话,但更加技术化。


Scott,就像Igor的回答一样,我仍然想知道:你能否将属性声明为命名空间而不是类型?你可以这样写public System IAmANamespace { get; set; }吗?因为如果你不能这样做,那么编译器能否找到一个名为System的命名空间并不重要。我只关心它不能找到名为System的类型。在我的情况下,当前上下文中有一个名为Foo的类型。为什么编译器找不到它并使用它呢? - devuxer
你不能使用命名空间代替类型,这正是错误提示你的内容。你可能误以为Foo对象已在相同的上下文中声明。尝试右键单击给出错误的类名,看看是否在上下文菜单中获得“解析”选项。它应该列出相应的类/命名空间匹配项。 - scottm
1
Scott,我知道错误信息告诉我的是什么...我想说的是,没有理由存在这个错误。基本上,我们现在面临的是一种平局,我认为C#设计者选择了错误的解决方法。如果你有一个只能容纳某种类型的插槽,那么好的设计应该通过选择类型来打破平局,而不是选择命名空间,然后告诉我出错了。 - devuxer
@DanM,我的示例展示了你所描述的内容。在第一个示例中,没有定义任何Foo对象(除了命名空间)。在这种情况下,你会得到错误。在第二个示例中,定义了一个Foo对象,编译器也认可了这一点。 - scottm
@Scott,在您的示例中,DataContextClass 属于哪个命名空间? - devuxer
在这个例子中,它位于文件的全局命名空间中。您也可以将其放在自己的命名空间中,从而实现相同的效果。 - scottm

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接