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