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