Це Спарта, чи це?


121

Далі йде питання про інтерв'ю. Я придумав рішення, але не знаю, чому воно працює.


Питання:

Не змінюючи Spartaклас, напишіть якийсь код, який робить MakeItReturnFalseповернення false.

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

Моє рішення: (SPOILER)

public class Place
{
public interface Sparta { }
}

Але чому ж Spartaв MakeItReturnFalse()см {namespace}.Place.Spartaзамість {namespace}.Sparta?


5
Чи можете ви додайте сповіщення про спойлер чи щось до рішення? Я настільки зневажений, що не отримую шансів вирішити це самостійно. Питання справді дивовижне.
Karolis Kajenas

1
Я спочатку включав теги спойлерів, однак його спільнота редагувала кілька разів протягом життя цієї публікації. Вибач за це.
буді

1
Я не люблю назву цієї публікації; він більше підходить для codegolf.SE. Чи можемо ми змінити це на щось, що насправді описує питання?
Супухстар

3
Симпатична головоломка. Жахливе питання інтерв'ю, але мила головоломка. Тепер, коли ви знаєте, як і чому це працює, вам слід зняти цю складнішу версію: stackoverflow.com/q/41319962/88656
Ерік Ліпперт

Відповіді:


117

Але чому ж Spartaв MakeItReturnFalse()см {namespace}.Place.Spartaзамість {namespace}.Sparta?

В основному, тому що так говорять правила пошуку назв. У специфікації C # 5 відповідні правила іменування розміщені у розділі 3.8 ("Ім’я простору імен та назви типів").

Перша пара куль - усічена та помічена - читайте:

  • Якщо ім’я простору або імені має форму Iабо форму I<A1, ..., AK> [значить, у нашому випадку K = 0] :
    • Якщо K дорівнює нулю, а ім'я простору імен або типу відображається в оголошенні загального методу [nope, no generic method ]
    • В іншому випадку, якщо ім’я простору імен або типу відображається в оголошенні типу, то для кожного екземпляра типу T (§10.3.1), починаючи з типу екземпляра цього декларації типу і продовжуючи з типом примірника кожного класу, що вкладається, або декларація структури (якщо така є):
      • Якщо Kдорівнює нулю і декларація Tвключає параметр типу з ім'ям I, то простір імен або ім’я типу вказує на цей параметр типу. [Нія]
      • В іншому випадку, якщо ім'я простору імен або типу відображається в тілі декларації типу, T або будь-який з його базових типів містить вкладений доступний тип, що має параметри імені Iта Kтипу, то простір імен або ім’я типу наводить на це тип, побудований за допомогою заданих аргументів типу. [Бінго!]
  • Якщо попередні кроки виявилися невдалими, то для кожного простору імен N, починаючи з простору імен, в яких відбувається ім’я простору імен, або типу типу, продовжуючи кожен укладений простір імен (якщо такий є) і закінчуючи глобальним простором імен, оцінюються наступні кроки доки суб'єкт господарювання не знаходиться:
    • Якщо Kнуль і Iє ім'я простору імен в N, то ... [Так, це вдалося б ]

Отже, ця кінцева точка кулі - це те, що піднімає Sparta клас, якщо перша куля нічого не знаходить ... але коли базовий клас Placeвизначає інтерфейс Sparta, він виявляється, перш ніж ми розглянемо Spartaклас.

Зауважте, що якщо ви робите вкладений тип Place.Spartaклас, а не інтерфейс, він все одно компілює та повертається false- але компілятор видає попередження, оскільки знає, що екземпляр класу Spartaніколи не буде екземпляром класу Place.Sparta. Так само якщо ви зберігаєте Place.Spartaінтерфейс, але складете Spartaклас sealed, ви отримаєте попередження, оскільки жоден Spartaекземпляр не міг реалізувати інтерфейс.


2
Ще одне випадкове спостереження: Використовуючи оригінальний Spartaклас, this is Placeповертає true. Однак додавання public interface Place { }до Spartaкласу викликає this is Placeповернення false. Змушує мою голову крутитися.
буді

@budi: Правильно, тому що знову ж таки, попередня куля виявляється Placeінтерфейсом.
Джон Скіт

22

При вирішенні імені до його значення "близькість" визначення використовується для усунення неясностей. Яке б визначення не було "найближчим" - це те, що обирається.

Інтерфейс Spartaвизначається в базовому класі. Клас Spartaвизначений у просторі імен, що містять. Речі, визначені в базовому класі, "ближче", ніж речі, визначені в одному просторі імен.


1
І уявіть, якби пошук імен не працював таким чином. Тоді робочий код, що містить внутрішній клас, буде порушений, якщо хто-небудь трапився би додати клас вищого рівня з тим самим іменем.
dan04

1
@ dan04: Але натомість робочий код, який не містить вкладеного класу, порушується, якщо хтось може додати вкладений клас з тим же іменем, що і клас вищого рівня. Тож це не зовсім тотальний сценарій "виграшу".
Джон Скіт

1
@JonSkeet Я б сказав, що додавання такого вкладеного класу - це зміна області, на яку робочий код має розумні причини впливати і слідкувати за змінами. Додавання абсолютно непов'язаного класу вищого рівня набагато далі видаляється.
Ендже вже не пишається SO

2
@JonSkeet Це не лише проблема крихкого базового класу дещо іншим виглядом?
Жоао Мендес

1
@ JoãoMendes: Так, дуже багато.
Джон Скіт

1

Красиве запитання! Я хотів би додати трохи довше пояснення для тих, хто не робить C # щодня ... тому що це питання добре нагадує проблеми вирішення імен загалом.

Візьміть оригінальний код, трохи змінений наступними способами:

  • Давайте надрукуємо назви типів, а не порівнюємо їх як у вихідному виразі (тобто return this is Sparta).
  • Давайте визначимо інтерфейс Athenaу Placeсуперкласі для ілюстрації роздільної здатності імені інтерфейсу.
  • Давайте також надрукуємо ім'я типу, яке thisвоно пов'язане в Spartaкласі, щоб зробити все дуже зрозумілим.

Код виглядає так:

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

Таким чином, ми ефективно переплутали порівняння типів у MakeItReturnFalseвизначенні функції, просто визначивши інтерфейс Спарти в надкласі, який знаходимо спочатку за роздільною здатністю імені.

Але чому C # вирішив розставити пріоритети інтерфейсам, визначеним в суперкласі в роздільній здатності імені? @JonSkeet знає! І якщо ви прочитаєте його відповідь, ви отримаєте деталі протоколу вирішення імені в C #.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.