воскресенье, 3 ноября 2013 г.

Jon Skeet's Quiz

Блог переехал. Актуальная версия поста находится по адресу: http://aakinshin.net/ru/blog/dotnet/jon-skeet-quiz/.


Однажды Джона Скита попросили сформулировать три интересных вопроса на знание C#. Он спросил следующее (оригинал вопросника, перевод статьи):

Q1. Вызов какого конструктора можно использовать, чтобы следующий код вывел True (хотя бы в реализации Microsoft.NET)?

object x = new /* fill in code here */;
object y = new /* fill in code here */;
Console.WriteLine(x == y);

Учтите, что это просто вызов конструктора, вы не можете поменять тип переменных.

Q2. Как сделать так, чтобы следующий код вызывал три различных перегрузки метода?

void Foo()
{
    EvilMethod<string>();
    EvilMethod<int>();
    EvilMethod<int?>();
}

Q3. Как заставить следующий код выбросить исключение во второй строчке с помощью локальной переменной (без хитрого изменения её значения)?

string text = x.ToString(); // No exception
Type type = x.GetType(); // Bang!

Вопросы показались мне интересными, поэтому я решил обсудить их решения.


A1-1. Одним из самых простых способ является использование Nullable-типов:

object x = new int?();
object y = new int?();
Console.WriteLine(x == y);

Несмотря на явный вызов конструктора, получившиеся значения равны null, а следовательно совпадают.

A1-2. Или можно вспомнить про интернирование строк и объявить две пустые строчки:

object x = new string(new char[0]);
object y = new string(new char[0]);
Console.WriteLine(x == y);

A2. Вторая задачка — самая сложная из трёх предложенных. Необходимо придумать такое решение, чтобы запускались именно три разных перегрузки нашего метода. В качестве варианта решения можно рассмотреть следующий код:

public class ReferenceGeneric<T> where T : class { }

public class EvilClassBase
{
  protected void EvilMethod<T>()
  {
    Console.WriteLine("int?");
  }
}

public class EvilClass : EvilClassBase
{
  public void Run()
  {
    EvilMethod<string>();
    EvilMethod<int>();
    EvilMethod<int?>();
  }

  private void EvilMethod<T>(ReferenceGeneric<T> arg = null) where T : class
  {
    Console.WriteLine("string");
  }

  private void EvilMethod<T>(T? arg = null) where T : struct
  {
    Console.WriteLine("int");
  }
}

Для начала разберёмся с типам string и int. Тут всё просто: string является ссылочным типом, а int — значимым. При написании кода нам помогут конструкции where T : class, where T : struct и параметры по умолчанию, которые явно задействуют тип T соответствующим образом: в первый метод пойдёт аргумент типа ReferenceGeneric<T> (он может принимать только ссылочные типы), а во второй — T? (он может принимать только значимые non-nullable типы). Теперь вызовы EvilMethod<string>() и EvilMethod<int>() «найдут» себе правильные перегрузки.

Едем дальше, вспомним про int?. Для него создадим перегрузку с сигнатурой без всяких дополнительных условий EvilMethod<T>() (увы, C# не позволяет написать что-нибудь вроде where T : Nullable<int>). Но если мы объявим такой метод в том же классе, то он «заберёт» себе вызовы первых двух методов. Поэтому следует «отправить» его в базовый класс, там он нам мешать не будет.

Давайте взглянем на то, что получилось. Вызовы EvilMethod<string>() и EvilMethod<int>() «увидят» подходящие перегрузки в текущем классе и будут их использовать. Вызов EvilMethod<int?>() подходящей перегрузки в текущем классе «не найдёт», поэтому «пойдёт» за ней в базовый класс. Сила C# Overload resolution rules опять помогла нам!


A3. И снова Nullable-типы спешат на помощь!

var x = new int?();
string text = x.ToString(); // No exception
Type type = x.GetType(); // Bang!

Вспомним, что метод ToString() перегружен в Nullable<T>, для null-значения он вернёт пустую строчку. Увы, для GetType() такой фокус не пройдёт, он не может быть перегружен и на null-значении выбросит исключение. Также вы можете почитать оригинальный ответ Джона на свой вопрос.

Не забываем, что при очень большом желании через неуправляемый код мы всегда можем долезть до таблицы методов и ручками подменить ссылку на GetType(), но сегодня нас просили не хитрить =).

2 комментария:

  1. Спасибо за разбор задач, было интересно посмотреть ваше решение задачи номер №2.

    Кстати, задачу №3 можно решить и другим способом, однако не таким изящным.
    Пример:

    internal sealed class Program
    {
    public static void Main()
    {
    dynamic x = new ExpandoObject();
    x.GetType = new Func(() => { throw new Exception(); });

    string text = x.ToString(); // No exception
    Type type = x.GetType(); // Bang!
    }
    }

    ОтветитьУдалить