This is Sparta, or is it?

121
以下是一道面试题。我想出了一个解决方案,但不确定它为什么有效。

问题:

在不修改Sparta类的情况下,编写一些代码使MakeItReturnFalse返回false

public class Sparta : Place
{
    public bool MakeItReturnFalse()
    {
        return this is Sparta;
    }
}

我的解决方案:(剧透)

public class Place
{
public interface Sparta { }
}

但为什么MakeItReturnFalse()方法中的Sparta指的是{namespace}.Place.Sparta而不是{namespace}.Sparta呢?


5
请在答案中加上“剧透警告”或类似的东西好吗?我很失望,因为我没能有机会自己解决这个问题。不过问题确实很棒。 - Karolis Kajenas
1
我最初包含了剧透标签,但是在这篇文章的生命周期中,它被社区多次编辑删除了。对此我感到抱歉。 - budi
1
我不喜欢这篇文章的标题;它更适合于codegolf.SE。我们能否将其更改为实际描述问题的内容? - Ky -
3
可爱的谜题。虽然这是一个糟糕的面试问题,但是它很可爱。现在你知道了这个问题的解法和原理,你应该尝试一下更难的版本:https://dev59.com/Tp3ha4cB1Zd3GeqPXaXX - Eric Lippert
3个回答

116
为什么在MakeItReturnFalse()中的Sparta指的是{namespace}.Place.Sparta而不是{namespace}.Sparta呢?基本上,这是由命名规则决定的。在C# 5规范中,相关的命名规则在第3.8节(“命名空间和类型名称”)中。前几个项目-被截断和注释-如下所示:如果命名空间或类型名称的形式为II<A1, ..., AK>[因此,在我们的情况下,K=0]:
  • 如果K为零并且命名空间或类型名称出现在通用方法声明中[没有,没有通用方法]
  • 否则,如果命名空间或类型名称出现在类型声明中,则对于每个实例类型T(§10.3.1),从该类型声明的实例类型开始,并继续使用每个封闭类或结构声明的实例类型(如果有的话):
    • 如果K为零并且T的声明包括一个名为I的类型参数,则命名空间或类型名称引用该类型参数[没有]
    • 否则,如果命名空间或类型名称出现在类型声明的正文中,并且T或其任何基类型包含具有名称I和K类型参数的嵌套可访问类型,则命名空间或类型名称引用使用给定类型参数构造的该类型[成功!]
如果前面的步骤不成功,则对于每个命名空间N,从命名空间-或-type-name出现的命名空间开始,继续使用每个封闭命名空间(如果有的话),并以全局命名空间结束,直到找到实体:
  • 如果K为零且I是N中的命名空间的名称,则... [是的,这将成功]
因此,如果第一个项目没有找到任何内容,那么最后一个项目就会捕捉到Sparta类...但是当基类Place定义了一个接口Sparta时,在考虑Sparta类之前,它会被找到。
请注意,如果您将嵌套类型Place.Sparta更改为类而不是接口,则它仍会编译并返回false,但编译器会发出警告,因为它知道Sparta的实例永远不会是Place.Sparta类的实例。同样,如果您保持Place.Sparta为接口,但使Spartasealed,则会收到警告,因为没有Sparta实例可以实现该接口。

2
另一个随机观察:使用原始的 Sparta 类,this is Place 返回 true。但是,将 public interface Place {} 添加到 Sparta 类会导致 this is Place 返回 false。让我感到困惑。 - budi
@budi:没错,因为前面的项目将Place识别为接口。 - Jon Skeet

22

在将名称解析为其值时,使用定义的“接近程度”来解决歧义。选择最接近的定义。

接口Sparta在基类中定义。类Sparta在包含命名空间中定义。在基类中定义的内容比在同一个命名空间中定义的内容更“接近”。


1
想象一下,如果名称查找不是这样工作的。那么,包含内部类的工作代码将变得无效,如果有人恰好添加了一个同名的顶级类。 - dan04
1
@dan04:但是,相反,如果有人添加了一个与顶层类同名的嵌套类,则不包含嵌套类的工作代码将被破坏。因此,这并不完全是一个“胜利”的场景。 - Jon Skeet
1
@JonSkeet 我认为添加这样一个嵌套类是在工作代码有合理原因受到影响的区域进行更改,并且需要注意变化。添加一个完全不相关的顶级类则远离得多。 - Angew is no longer proud of SO
2
@JonSkeet 这不就是脆弱基类问题的一个稍微不同的体现吗? - João Mendes
1
@JoãoMendes:是的,基本上是这样。 - Jon Skeet
@dan04:不一定。C++使用注入类名来解决这个问题。也就是说,当前类和包含类的名称与成员具有相同的优先级进行查找,从而防止了这个特定问题的出现。但对于根本没有typedef的C#来说,预定义这样的类型别名会很奇怪。 - Ben Voigt

1

好问题!我想为那些不经常使用C#的人添加稍微长一点的解释……因为这个问题是一个很好的提醒,涉及到了名称解析问题。

接下来稍微修改原始代码:

  • 我们将打印类型名称,而不是像原始表达式中进行比较(即return this is Sparta)。
  • 我们将在Place超类中定义接口Athena,以说明接口名称解析。
  • 我们还将打印出Sparta类中绑定的this的类型名称,以使一切非常清晰明了。

代码如下:

public class Place {
    public interface Athena { }
}

public class Sparta : Place
{
    public void printTypeOfThis()
    {
        Console.WriteLine (this.GetType().Name);
    }

    public void printTypeOfSparta()
    {
        Console.WriteLine (typeof(Sparta));
    }

    public void printTypeOfAthena()
    {
        Console.WriteLine (typeof(Athena));
    }
}

我们现在创建一个Sparta对象并调用三个方法。
public static void Main(string[] args)
    {
        Sparta s = new Sparta();
        s.printTypeOfThis();
        s.printTypeOfSparta();
        s.printTypeOfAthena();
    }
}

我们得到的输出是:


Sparta
Athena
Place+Athena

然而,如果我们修改Place类并定义接口Sparta:
   public class Place {
        public interface Athena { }
        public interface Sparta { } 
    }

那么,这个 Sparta -- 这个接口 -- 将首先对名称查找机制可用,并且我们代码的输出将发生变化:

Sparta
Place+Sparta
Place+Athena

所以,我们通过在超类中定义Sparta接口,影响了MakeItReturnFalse函数定义中的类型比较,因为它首先被名称解析找到。
但是,为什么C#选择优先使用在超类中定义的接口进行名称解析?@JonSkeet知道!如果您阅读他的答案,您将了解C#中名称解析协议的详细信息。

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