Блог переехал. Актуальная версия поста находится по адресу: 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.
ОтветитьУдалитьКстати, задачу №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!
}
}
Спасибо, у вас весьма интересный способ.
Удалить