Jon Skeet's Quiz

.NET C#

Однажды Джона Скита попросили сформулировать три интересных вопроса на знание 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, а следовательно совпадают.

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(), но сегодня нас просили не хитрить =).